Preserve homestead context and garden-centred Blender design

This commit is contained in:
2026-09-10 20:52:43 +10:00
commit 46648f2687
321 changed files with 17843 additions and 0 deletions

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
*.blend binary
*.uasset binary
*.umap binary
*.fbx binary
*.glb binary
*.png binary
*.jpg binary
*.svg text eol=lf
*.md text eol=lf
*.py text eol=lf
*.json text eol=lf

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Full dated archives already live beside this repository.
/snapshots/
.DS_Store
*.blend1
*.blend2
__pycache__/
*.pyc
unreal/**/DerivedDataCache/
unreal/**/Intermediate/
unreal/**/Saved/
unreal/**/Binaries/
# Whole-server archive checksum list includes snapshots/legacy file policy; Git has its own manifest.
/manifest-sha256.json

16
AGENTS.md Normal file
View File

@@ -0,0 +1,16 @@
# Working on the Samford Valley home
Read README.md, docs/PROJECT-CONTEXT.md, docs/REQUEST-HISTORY.md and the latest decision record before editing.
- Current model: blender/samford-solarpunk-garden-flow-tour.blend, version 004.
- The table-tennis deck was explicitly removed. Do not restore it from older scenes.
- Future child/dwelling pavilions remain ghost outlines. The one built small pavilion is a meditation room.
- Preserve the detailed kitchen, reading library, bedroom sleep setup, sauna, shower, solar, tanks, Starlink, homelab, Tesla garage, training and food-growing features.
- Keep new layout geometry, plan coordinates, fast display copies, tour cameras and captions consistent.
- Blender collections: DETAIL is full editable render geometry; FAST TOUR is the simplified viewport. A hidden DETAIL collection can have stale matrix_world values during scripting; explicitly construct transforms or evaluate the collection before applying them.
- Incremental source scripts depend on particular prior models. Do not run the whole script history against the current scene; it would duplicate or misplace geometry. Read each script and choose its expected starting model first.
- Validate saved .blend content and inspect actual renders before claiming a scene edit is finished.
- Store a dated decision and a Git commit for intentional revisions. Preserve earlier model versions unless a cleanup is explicitly requested.
- Working files and context stay in the private repository. The public website contains selected images and concept notes.
- This is an illustrative, unsurveyed concept. Do not label materials, engineering, equipment loads, costs or property compliance as verified when they are not.
- No need to create Linear issues unless Marcus explicitly asks.

76
README.md Normal file
View File

@@ -0,0 +1,76 @@
# Samford Valley home
A long-term working project for a modest, extendible family homestead in Samford Valley, Queensland. Started September 2026. The aim is to refine it over several years: real site surveys and LiDAR, coordinated 3D plans, Blender studies of the build, and eventually a full Unreal Engine walkthrough.
## Repository and context
Private Git repository: https://git.rehbock.xyz/marcus/samford-valley-home
Read [Project context](docs/PROJECT-CONTEXT.md), [Request history](docs/REQUEST-HISTORY.md) and [Agent notes](AGENTS.md) for the full brief, decisions, rejected ideas and current state. Git contains the actual Blender binaries, not download placeholders. The server working copy is `/home/marcus/projects/samford-valley-home`. Compressed snapshots are kept alongside it but excluded from Git.
Clone using `git clone ssh://git@git.rehbock.xyz:2222/marcus/samford-valley-home.git`. Current files use ordinary Git; no Git LFS client is needed. The existing Git server has LFS disabled.
## Open the current work
- **Blender 5.2.1:** `blender/samford-solarpunk-garden-flow-tour.blend`. Includes the entire homestead, reading chair and 163 books. Its camera tour has 30 chapters and lasts 2 minutes 30 seconds. Hover over the 3D view and press Space to play; Shift + Left Arrow returns to the start. It opens at the overview of the revised garden layout. Earlier bedroom and reading-tour files remain as previous versions.
- **Unreal Engine 5.8.2:** `unreal/SamfordMeditation/SamfordMeditation.uproject`. The saved meditation room includes 152 meshes, 14 materials, lighting and a camera. The Unreal work currently covers this room only; an interactive whole-home walkthrough is future work.
- **Images:** `renders/` contains the latest compound, detailed kitchen, training terrace and outdoor dining, plus earlier bedroom, reading corner, garage and actual Unreal room studies.
- **Concept layout:** `plans/concept/garden-flow-plan.svg`. Current version 004 replaces the table-tennis deck with flowers, aligns the pavilions and adds curved paths and a central garden court.
- **Animation instructions:** `walkthroughs/UNREAL-CAMERA-ANIMATION.md`.
## Working in Blender
The FAST TOUR collection is a lightweight display copy for smooth playback. Full editable objects are in DETAIL - full geometry for renders. For detailed edits, enable DETAIL in the Outliner’s monitor column and disable FAST TOUR. Cycles renders use the detailed scene automatically. The display copy must be regenerated after geometry edits to reflect those changes during the fast tour.
## Folder map
| Folder | Purpose |
| --- | --- |
| blender/ | Current editable house models and camera-tour notes |
| unreal/ | Unreal project, portable FBX/GLB room transfer and textures |
| renders/ | Dated screenshots and rendered stills |
| site/surveys/ | Boundary, contour and survey-control information when available |
| site/lidar/raw/ | Original unmodified LiDAR scans when captured |
| site/lidar/processed/ | Registered, cleaned and reduced point clouds |
| site/photos/ | Site and construction photos |
| plans/concept/ | Early design sketches and layouts |
| plans/architectural/ | Future architectural drawings and coordinated models |
| plans/engineering/ | Future structural and services documents |
| references/ | Research, material studies and conversion scope |
| decisions/ | Dated changes, reasons and open questions |
| walkthroughs/ | Camera-animation notes and future exported films |
| snapshots/ | Dated frozen project archives with checksums |
The survey, LiDAR and professional-plan folders are intentionally empty. The current hillside is illustrative and has no surveyed property boundary, georeferencing or verified site dimensions. These concept models are not construction documents.
## Preserve the work over years
Keep one clear current model, and save milestone versions before major layout changes. Use names such as `2026-09-10-v001-concept`. Record what changed and why in decisions/. Before importing a site scan, record the survey date, units, horizontal coordinate reference, vertical datum and agreed local origin; retain the untouched original scan. Keep site coordinates and detailed working files in this project archive unless deliberately publishing them.
The initial snapshot is a milestone copy on this server, not an independent off-site backup. Regular independent backup and restore checks should be set up before unique site scans or construction records become the only copy here. Automatic backups have not been configured by this upload.
## Public project page
https://rehbock.xyz/projects/samford-valley-home/
The public page contains selected concept screenshots and progress notes. The editable project files are stored separately on the server at `/home/marcus/projects/samford-valley-home`.
## Bedroom revision — 10 September 2026, v002
The bed now has an Eight Sleep Pod-style fitted cover, bedside cooling Hub and routed hoses. Two oatmeal-faced curtains have opaque blackout lining and a closed shape-key position. A split AC head is mounted high on the west wall, with a condenser behind the bedroom wing and exposed service trunking. Product geometry is indicative; a specific Pod model and HVAC capacity have not been selected.
To close the curtains: enable DETAIL, disable FAST TOUR, select each BLACKOUT CURTAIN object, then open Object Data Properties → Shape Keys → Closed - blackout and set Value to 1. Set both panels back to 0 for the open presentation view. The fast display copy always shows the open curtains.
## Daily life and kitchen revision — 10 September 2026, v003
Added a covered training deck with a dedicated steel pull-up/rings frame, two adjustable dumbbells, an adjustable bench and movable mats. The gathering terrace seats ten with a nearby tea/matcha counter. A separate level deck holds a folding table-tennis table; a dashed 10 × 10 m basketball area south of the house remains an unbuilt reserve away from the gravel driveway. A garden walking loop reaches a shaded quiet bench and harvest bench. Breakfast preparation, pantry and freezer storage complete the everyday food setup.
The kitchen now has detailed timber cabinetry, an oven, a four-zone induction cooktop, extraction, a recessed sink and copper tap, stainless-lined copper cookware and a 2.2 × 0.95 m marble-topped island. Approximate clear distance between the island and rear counter is 1.7 m. Appliance models, structural loads, access and clearances need design development; the exercise frame is a load-rated design intent, not an engineering certification.
The saved camera tour includes all additions. Full Cycles scene settings are 2560 × 1440; published preview renders use 60% resolution (1536 × 864). The Unreal meditation-room project remains unchanged.
## Garden-flow revision — 10 September 2026, v004
Removed the table-tennis deck and all equipment. Aligned the garage with the main pavilion and the meditation room with the parents’ wing. The future east pavilion balances the meditation room; a second future footprint continues the central axis north. Added an oval garden court, curved covered timber breezeway, smooth gravel walks and driveway, 13 flowering beds and eight flowering shrubs. The former play area is now a flower garden. Two concept eucalyptus trees were moved clear of the solar array and vehicle route. The future basketball area remains unbuilt. Updated the 30-chapter tour and refreshed the overview, courtyard, flowers, dining and training renders.
The full conversation context and decision history are now preserved in docs/. Source scripts are retained under source/ for future editing; they are incremental historical tools and must be run against their correct starting model. Survey and LiDAR folders remain reserved for future real-site work.

View File

@@ -0,0 +1,11 @@
{
"eight_sleep_cover": 1,
"eight_sleep_hub": 1,
"blackout_curtain_panels": 2,
"curtain_closed_shape_keys": 2,
"indoor_ac": 1,
"outdoor_condenser": 1,
"tour_chapters": 24,
"tour_seconds": 120,
"new_fast_display_meshes": 1
}

View File

@@ -0,0 +1,34 @@
{
"training_area": 1,
"pullup_bar": 1,
"wooden_rings": 2,
"adjustable_dumbbells": 2,
"adjustable_bench": 1,
"exercise_mats": 2,
"outdoor_seats": 10,
"tea_counter": 1,
"folding_table_tennis": 1,
"basketball_status": "Future reserve only",
"walking_loop": true,
"quiet_bench": 1,
"breakfast_counter": 1,
"pantry": 1,
"freezer": 1,
"harvest_bench": 1,
"tour_chapters": 30,
"tour_seconds": 150,
"kitchen": {
"oven": 1,
"induction_zones": 4,
"copper_stockpot": 1,
"copper_saucepan": 1,
"copper_frying_pan": 1,
"marble_island_m": [
2.2,
0.95
],
"island_rear_work_aisle_m": 1.67,
"old_simplified_kitchen_removed": true,
"fast_furniture_refreshed": true
}
}

View File

@@ -0,0 +1,97 @@
{
"training": [
-12.2,
-18,
4.8,
5.4
],
"outdoor_table": [
10,
3.5,
6,
5.8
],
"table_tennis": [
11,
-8.5,
5.5,
6.5
],
"future_basketball": [
13,
-36,
10,
10
],
"walking_loop": [
[
-11,
14.7
],
[
-14,
18
],
[
-11,
23
],
[
1,
24
],
[
13,
23
],
[
20,
19
],
[
19,
10
],
[
30,
10
],
[
33,
3
],
[
33,
-10
],
[
27,
-13
],
[
20,
-11
],
[
17,
-4
],
[
14,
1
],
[
14,
10
],
[
19,
10
]
],
"harvest_bench": [
29.8,
1.5
],
"outdoor_seats": 10
}

View File

@@ -0,0 +1,198 @@
{
"chapters": 30,
"end_frame": 3600,
"fps": 24,
"missing": [],
"caption_issues": [],
"caption_samples": [
{
"frame": 31,
"titles": [
"01 / THE SPACIOUS HOMESTEAD"
]
},
{
"frame": 151,
"titles": [
"02 / LIVING PAVILION & VERANDA"
]
},
{
"frame": 271,
"titles": [
"03 / KITCHEN & EXPOSED STRUCTURE"
]
},
{
"frame": 391,
"titles": [
"04 / DINING & OPEN LIVING"
]
},
{
"frame": 511,
"titles": [
"05 / ONE SERVER - LOCAL AI"
]
},
{
"frame": 631,
"titles": [
"06 / SHORT CORRIDOR TO PRIVATE ROOMS"
]
},
{
"frame": 751,
"titles": [
"07 / ADJACENT BEDROOM WING"
]
},
{
"frame": 871,
"titles": [
"08 / ENCLOSED BEDROOM"
]
},
{
"frame": 991,
"titles": [
"09 / BATHROOM - SEPARATE LOBBY DOOR"
]
},
{
"frame": 1111,
"titles": [
"10 / FINNISH SAUNA & OUTDOOR SHOWER"
]
},
{
"frame": 1231,
"titles": [
"11 / SAUNA BENCHES & STONE HEATER"
]
},
{
"frame": 1351,
"titles": [
"12 / OUTDOOR SHOWER"
]
},
{
"frame": 1471,
"titles": [
"13 / TWO RAINWATER TANKS"
]
},
{
"frame": 1591,
"titles": [
"14 / ROOFTOP SOLAR & STARLINK"
]
},
{
"frame": 1711,
"titles": [
"15 / SOLAR ARRAY & ENERGY STORAGE"
]
},
{
"frame": 1831,
"titles": [
"16 / FOOD GARDEN & GREENHOUSE"
]
},
{
"frame": 1951,
"titles": [
"17 / INSIDE THE GREENHOUSE"
]
},
{
"frame": 2071,
"titles": [
"18 / QUIET MEDITATION ROOM"
]
},
{
"frame": 2191,
"titles": [
"19 / CUSHION, CANDLES & STILLNESS"
]
},
{
"frame": 2311,
"titles": [
"20 / GRAVEL ROAD & GARAGE"
]
},
{
"frame": 2431,
"titles": [
"21 / TESLA, SOLAR CHARGING & WORKSHOP"
]
},
{
"frame": 2551,
"titles": [
"22 / FUTURE SPACES - BUILD LATER"
]
},
{
"frame": 2671,
"titles": [
"23 / A HOME THAT CAN CHANGE"
]
},
{
"frame": 2791,
"titles": [
"24 / BOOKS & A QUIET READING CORNER"
]
},
{
"frame": 2911,
"titles": [
"25 / TRAINING & STRENGTH"
]
},
{
"frame": 3031,
"titles": [
"26 / OUTDOOR TABLE & MATCHA"
]
},
{
"frame": 3151,
"titles": [
"27 / PLAY & FUTURE BASKETBALL"
]
},
{
"frame": 3271,
"titles": [
"28 / WALKING & FOOD GARDEN"
]
},
{
"frame": 3391,
"titles": [
"29 / EASY EVERYDAY FOOD"
]
},
{
"frame": 3511,
"titles": [
"30 / COPPER & MARBLE KITCHEN"
]
}
],
"render_engine": "CYCLES",
"render_size": [
2560,
1440
],
"detail_render_enabled": true,
"fast_excluded_from_render": true,
"training_steps": 3,
"play_steps": 3
}

View File

@@ -0,0 +1,11 @@
{
"deleted_objects": 143,
"moved_objects": 1655,
"flower_beds": 13,
"planted_clumps": 221,
"flowering_shrubs": 8,
"smooth_routes": 13,
"table_tennis_removed": true,
"future_pavilions": 2,
"tour_chapters": 30
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"no_table_tennis": true,
"garage_center": [
0.0,
-19.0
],
"meditation_center": [
-11.8,
12.0
],
"caption_issues": [],
"tour_chapters": 30,
"rainwater_tanks": 2,
"kitchen_preserved": true,
"flower_beds": 13,
"render_size": [
2560,
1440,
100
],
"fast_excluded_from_render": true,
"pavilion_path_intersections": [],
"solar_tree_clearance": true
}

View File

@@ -0,0 +1,8 @@
{
"books": 163,
"bookshelf_bays": 2,
"reading_chairs": 1,
"tour_chapters": 24,
"tour_seconds": 120,
"fast_proxy_objects_added": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,218 @@
import bpy,math,os,json
from mathutils import Vector,Matrix
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene
detail=bpy.data.collections['DETAIL - full geometry for renders']
col=bpy.data.collections.new('33 Daily life - training gathering play and food');detail.children.link(col)
COL={'life':col};active='life'
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];roof=bpy.data.materials['Pale Colorbond - powder coated steel'];linen=bpy.data.materials['Undyed flax linen'];gravel=bpy.data.materials['Loose local gravel - fine stones'];F=1.12
rubber=mat('Exercise mat muted charcoal',(.035,.048,.044),.94)
sage=mat('Table tennis muted green',(.10,.21,.16),.63)
cream=mat('Ceramic tea bowls warm white',(.73,.68,.56),.48)
white=mat('Table tennis painted court lines',(.8,.79,.71),.63)
black=mat('Freezer seals and equipment grips',(.028,.03,.029),.7)
metal=mat('Appliance brushed metal',(.44,.46,.43),.36,.7)
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
def path(n,pts,w):
vs=[];fs=[]
for a,b in zip(pts,pts[1:]):
a,b=Vector(a),Vector(b);delta=b-a;perp=Vector((-delta.y,delta.x)).normalized()*w/2;steps=max(2,int(delta.length/.3));off=len(vs)
for i in range(steps+1):
q=a+delta*i/steps
for v in [q-perp,q+perp]:vs.append((v.x,v.y,ground(v.x,v.y)+.085))
for i in range(steps):k=off+2*i;fs.append((k,k+1,k+3,k+2))
return mesh(n,vs,fs,gravel)
def deck(n,cx,cy,w,d,z):
box(n+' accessible pier deck',(cx,cy,z-.09),(w,d,.18),wood,.01)
for x in [cx-w/2+.15,cx+w/2-.15]:
for y in [cy-d/2+.15,cy+d/2-.15]:
g=ground(x,y);box(n+' stone pier',(x,y,(g+z-.18)/2),(.3,.3,max(.08,z-.18-g)),stone,.01)
for i in range(int(d/.15)):
box(n+' deck board joint',(cx,cy-d/2+.075+i*.15,z+.002),(w,.006,.003),steel)
def bench(n,x,y,z,length=2.7):
box(n+' seat',(x,y,z+.45),(length,.43,.065),wood,.015)
box(n+' linen seat pad',(x,y,z+.502),(length-.1,.39,.04),linen,.016)
for xx in [x-length*.37,x+length*.37]:box(n+' leg',(xx,y,z+.21),(.07,.35,.42),woodz,.008)
box(n+' backrest',(x,y+.21,z+.75),(length,.055,.27),wood,.012)
def ring(n,c,r,t,material):
cx,cy,cz=c;vs=[];fs=[];N=48;M=10
for i in range(N):
a=i*2*pi/N
for j in range(M):
b=j*2*pi/M;rr=r+t*cos(b);vs.append((cx+rr*cos(a),cy+t*sin(b),cz+rr*sin(a)))
for i in range(N):
for j in range(M):fs.append((i*M+j,((i+1)%N)*M+j,((i+1)%N)*M+(j+1)%M,i*M+(j+1)%M))
ob=mesh(n,vs,fs,material)
for p in ob.data.polygons:p.use_smooth=True
return ob
# Covered exercise pavilion west of the garage. Independent steel exercise frame.
tx,ty,tz=-12.2,-18,.95
deck('TRAINING',tx,ty,4.8,5.4,tz)
for x in [tx-2.28,tx+2.28]:
for y,h in [(ty-2.57,2.95),(ty+2.57,3.25)]:
box('Training timber canopy post',(x,y,tz+h/2),(.125,.125,h),woodz,.005)
box('Training canopy steel shoe',(x,y,tz+.07),(.16,.16,.14),steel,.005)
for x in [tx-2.28+i*.57 for i in range(9)]:beam('Training exposed roof rafter',(x,ty-2.9,tz+2.95),(x,ty+2.9,tz+3.29),.065,.18,wood)
ro=box('Training pale mono pitch canopy',(tx,ty,tz+3.16),(5.3,6,.065),roof,.006);ro.rotation_euler.x=math.atan(.3/5.4)
for x in [tx-.98,tx+.98]:
box('EXERCISE FRAME - steel upright',(x,ty+.85,tz+1.4),(.095,.095,2.8),steel,.006)
box('Exercise frame bolted foot plate',(x,ty+.85,tz+.02),(.30,.36,.04),steel,.005)
for dx in [-.1,.1]:
for dy in [-.12,.12]:rod('Exercise frame anchor concept',(x+dx,ty+.85+dy,tz+.04),(x+dx,ty+.85+dy,tz+.067),.011,metal,sides=8)
bar=rod('PULL UP BAR - dedicated steel frame',(tx-1.02,ty+.85,tz+2.70),(tx+1.02,ty+.85,tz+2.70),.017,steel,sides=20)
bar['Design intent']='Dedicated exercise frame intended to be load rated by its manufacturer or structural engineer. These concept anchor plates do not establish a safe working load.'
for x in [tx-.28,tx+.28]:
box('Adjustable ring hanging strap',(x,ty+.85,tz+2.10),(.027,.009,1.18),black,.002)
ring('WOODEN GYMNASTIC RING',(x,ty+.85,tz+1.40),.125,.017,wood)
for x,y in [(tx-.65,ty-.5),(tx+.55,ty-.5)]:box('MOVABLE EXERCISE MAT',(x,y,tz+.022),(.85,1.85,.045),rubber,.025)
box('Adjustable weight bench seat',(tx+1.55,ty-.88,tz+.45),(.40,.38,.09),rubber,.035)
back=box('ADJUSTABLE BENCH - inclined back',(tx+1.55,ty-.25,tz+.60),(.40,.91,.09),rubber,.035);back.rotation_euler.x=.3
beam('Bench adjustable steel spine',(tx+1.55,ty-1.05,tz+.15),(tx+1.55,ty+.23,tz+.41),.065,.065,steel)
for y in [ty-1,ty+.15]:box('Bench stabilising foot',(tx+1.55,y,tz+.065),(.59,.065,.065),steel,.009)
box('Dumbbell timber stand',(tx-1.55,ty-.5,tz+.72),(.68,1.25,.06),wood,.012)
for y in [ty-.95,ty-.05]:
box('Dumbbell stand leg',(tx-1.55,y,tz+.34),(.5,.06,.68),steel)
for y in [ty-.80,ty-.22]:
rod('ADJUSTABLE DUMBBELL grip',(tx-1.73,y,tz+.85),(tx-1.37,y,tz+.85),.019,steel,sides=16)
for x in [tx-1.77,tx-1.33]:
for dx in [-.06,-.02,.02,.06]:box('Dumbbell selectable weight plate',(x+dx,y,tz+.85),(.027,.22,.23),black,.008)
for i,x in enumerate([-9.60,-9.25,-8.90]):
top=.76-.19*i;box('Training access step',(x,-17,top-.08),(.36,1.2,.16),wood,.009)
path('Training to garage pedestrian link',[(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],1.1)
# Outdoor dining: a separate terrace keeps the original narrow veranda clear.
dx,dy=10,3.5;deck('GATHERING TERRACE',dx,dy,6,5.8,F)
deck('Veranda terrace connection',6.5,4.2,1,1.5,F)
box('OUTDOOR TABLE - ten place timber top',(dx,dy,F+.76),(3.2,1.03,.085),wood,.025)
for x in [dx-1.13,dx+1.13]:box('Outdoor table trestle',(x,dy,F+.36),(.10,.78,.72),woodz,.01)
bench('Outdoor north bench - four places',dx,dy+.98,F,2.95)
south_start=set(col.objects);bench('Outdoor south bench - four places',dx,dy-.98,F,2.95)
for o in set(col.objects)-south_start:
if 'backrest' in o.name:o.location.y=dy-1.19
for x in [dx-1.94,dx+1.94]:
box('Outdoor end chair seat',(x,dy,F+.45),(.47,.47,.06),wood,.012)
for a in [-.17,.17]:
for b in [-.17,.17]:box('Outdoor end chair leg',(x+a,dy+b,F+.21),(.038,.038,.42),woodz)
box('Outdoor end chair back',(x+(-.21 if x<dx else .21),dy,F+.75),(.055,.47,.36),wood,.01)
for x in [7.15,12.85]:
for y in [.75,6.25]:box('Gathering pergola post',(x,y,F+1.38),(.12,.12,2.76),woodz,.006)
for y in [.75,6.25]:box('Gathering pergola beam',(10,y,F+2.76),(6,.12,.20),wood,.005)
for x in [7.1+i*.35 for i in range(18)]:box('Gathering pergola shade slat',(x,3.5,F+2.88),(.065,5.8,.14),wood)
# Nearby movable tea counter and shared bowls.
box('TEA AND MATCHA - timber preparation cart',(11.6,5.74,F+.88),(1.55,.58,.06),wood,.016)
for x in [11,12.2]:
for y in [5.55,5.93]:box('Tea cart frame',(x,y,F+.43),(.045,.045,.86),woodz)
box('Tea cart storage shelf',(11.6,5.74,F+.25),(1.46,.50,.04),wood)
rod('Tea kettle',(11.15,5.72,F+.92),(11.15,5.72,F+1.13),.10,metal,sides=24)
for x in [11.5,11.76,12.02]:rod('Matcha ceramic bowl',(x,5.73,F+.92),(x,5.73,F+1.01),.075,cream,r2=.095,sides=24)
for x in [8.9,9.7,10.5,11.3]:
for y in [dy-.28,dy+.28]:rod('Shared table cup',(x,y,F+.81),(x,y,F+.89),.037,cream,sides=16)
# Table tennis has its own level deck, clear of vehicles; folding halves remain editable.
px,py,pz=11,-8.5,.80;deck('PLAY DECK',px,py,5.5,6.5,pz)
for y in [py-.695,py+.695]:
box('FOLDING TABLE TENNIS - hinged half',(px,y,pz+.76),(1.525,1.37,.045),sage,.004)
for x in [px-.50,px+.50]:
box('Table tennis folding leg',(x,y,pz+.38),(.035,.035,.71),steel,.004)
rod('Table tennis caster',(x-.025,y,pz+.035),(x+.025,y,pz+.035),.034,black,sides=12)
for x in [px-.746,px+.746,px]:box('Table tennis white longitudinal line',(x,py,pz+.784),(.012,2.73,.002),white)
for y in [py-1.36,py+1.36]:box('Table tennis white end line',(px,y,pz+.785),(1.52,.018,.002),white)
for x in [px-.82,px+.82]:rod('Table tennis net post',(x,py,pz+.76),(x,py,pz+.915),.007,steel,sides=8)
for i in range(7):rod('Table tennis horizontal net cord',(px-.82,py,pz+.77+i*.024),(px+.82,py,pz+.77+i*.024),.0015,white,sides=5)
for i in range(43):rod('Table tennis vertical net cord',(px-.8+i*.038,py,pz+.77),(px-.8+i*.038,py,pz+.914),.0012,white,sides=5)
for i,x in enumerate([8.05,7.70,7.35]):
top=.62-.18*i;box('Play deck access step',(x,-7,top-.08),(.36,1.2,.16),wood,.009)
path('Play deck pedestrian connection',[(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],1.1)
# A reserved footprint only: no court surface or working hoop is built now.
future=bpy.data.materials['Future shadow - faint sage outline']
for a,b in [((8,-41),(18,-41)),((18,-41),(18,-31)),((18,-31),(8,-31)),((8,-31),(8,-41))]:
a,b=Vector(a),Vector(b)
for i in range(10):
p=a+(b-a)*i/10;q=a+(b-a)*(i+.55)/10
beam('FUTURE BASKETBALL - dashed reserve',(p.x,p.y,ground(p.x,p.y)+.09),(q.x,q.y,ground(q.x,q.y)+.09),.07,.04,future)
# Walking loop: joins the quiet room and garden without crossing building reserves.
loop=[(-11,14.7),(-14,18),(-11,23),(1,24),(13,23),(20,19),(19,10),(30,10),(33,3),(33,-10),(27,-13),(20,-11),(17,-4),(14,1),(14,10),(19,10)]
path('SHADED WALKING LOOP - garden and quiet room',loop,1.05)
path('Walking loop return to home',[(14,1),(13,4),(13,7),(6,7)],1.05)
path('Walking loop home to meditation',[(-1,24),(-7,21),(-14,18)],1.05)
bench('QUIET GARDEN BENCH',30,10.9,ground(30,10.9)+.07,1.8)
for x in [29,31]:box('Quiet bench shade post',(x,11,ground(x,11)+1.2),(.10,.10,2.4),woodz,.006)
for x in [28.9+i*.26 for i in range(9)]:box('Quiet bench timber shade slat',(x,11,ground(30,11)+2.45),(.06,1.6,.10),wood)
# Everyday food: clear prep counter, pantry and compact freezer along the south wall.
box('BREAKFAST - clear preparation counter',(.22,-2.52,F+.94),(1.15,.73,.055),stone,.013)
box('Breakfast counter timber base',(.22,-2.55,F+.45),(1.10,.62,.89),wood,.009)
box('Breakfast chopping board',(.30,-2.48,F+.982),(.42,.30,.025),wood,.008)
for x in [-.19,.02]:rod('Breakfast cereal jar',(x,-2.73,F+.98),(x,-2.73,F+1.18),.066,cream,sides=20)
box('PANTRY - tall timber cabinet',(1.32,-2.55,F+1.06),(.85,.65,2.12),wood,.014)
for x in [1.12,1.52]:box('Pantry door pull',(x,-2.21,F+1.05),(.023,.028,.23),steel,.004)
box('FREEZER - compact upright',(2.27,-2.55,F+.66),(.72,.69,1.32),cream,.025)
box('Freezer insulated door',(2.27,-2.19,F+.68),(.68,.05,1.23),metal,.014)
box('Freezer door handle',(2.52,-2.14,F+.89),(.025,.04,.32),steel,.007)
for x in [2.04,2.5]:box('Freezer ventilation clearance foot',(x,-2.54,F+.035),(.065,.48,.07),black)
hx,hy=29.8,1.5;hz=ground(hx,hy)+.08
box('HARVEST BENCH - garden sorting surface',(hx,hy,hz+.90),(.78,2.05,.065),wood,.018)
for x in [hx-.28,hx+.28]:
for y in [hy-.81,hy+.81]:box('Harvest bench leg',(x,y,hz+.43),(.06,.06,.86),woodz,.006)
box('Harvest bench lower storage',(hx,hy,hz+.22),(.70,1.95,.045),wood)
for y in [hy-.6,hy+.10]:
box('Harvest timber produce crate',(hx,y,hz+1.045),(.57,.48,.24),wood,.012)
box('Harvest crate dark interior',(hx,y,hz+1.171),(.49,.4,.01),bpy.data.materials['Damp brown garden soil'])
layout={'training':[-12.2,-18,4.8,5.4],'outdoor_table':[10,3.5,6,5.8],'table_tennis':[11,-8.5,5.5,6.5],'future_basketball':[13,-36,10,10],'walking_loop':loop,'harvest_bench':[29.8,1.5], 'outdoor_seats':10}
json.dump(layout,open(OUT+'/daily-life-layout.json','w'),indent=2)
exec(compile(open(OUT+'/enhance_kitchen.py').read(),OUT+'/enhance_kitchen.py','exec'))
# A single merged viewport copy for new geometry.
verts=[];faces=[];mi=[];mats=[];mapping={}
for o in col.objects:
if o.type!='MESH':continue
remap=[]
for m in o.data.materials:
if m not in mapping:mapping[m]=len(mats);mats.append(m)
remap.append(mapping[m])
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
verts.extend(tuple(world@v.co) for v in o.data.vertices)
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mi.append(remap[p.material_index])
me=bpy.data.meshes.new('Daily life lightweight display');me.from_pydata(verts,[],faces);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',mi)
proxy=bpy.data.objects.new('FAST | Training gathering play and garden loop',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
cam=bpy.data.objects['PLAY TOUR camera']
shots=[('25 / TRAINING & STRENGTH',(-8,-12,4),(-8.8,-12.8,3.8),(-12.2,-18,2),25),('26 / OUTDOOR TABLE & MATCHA',(15,9,4.8),(14,8,4.4),(10,3.5,1.6),28),('27 / PLAY & FUTURE BASKETBALL',(17,-1,5),(16,-2,4.7),(11,-8.5,1),29),('28 / WALKING & FOOD GARDEN',(35,16,8),(33,14,7),(27,3,1.3),30),('29 / EASY EVERYDAY FOOD',(-.3,.4,2.9),(.1,.1,2.8),(1,-2.5,2),24)]
caption_col=bpy.data.collections['22 PLAY - guided camera tour']
ref=max((o for o in caption_col.objects if o.type=='FONT'),key=lambda o:o.name)
ref.scale=(0,0,0);ref.keyframe_insert('scale',frame=2881)
shots.append(('30 / COPPER & MARBLE KITCHEN',(-.8,2.5,3.1),(-1.0,2.2,2.95),(-3.45,-1.2,1.9),25))
chapters=json.load(open(OUT+'/tour-chapters-reading.json'))
for idx,(title,a,b,target,lens) in enumerate(shots):
start=2881+idx*120;end=start+119
for f,p in [(start,a),(end,b)]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=f);cam.keyframe_insert('rotation_euler',frame=f);cam.data.lens=lens;cam.data.keyframe_insert('lens',frame=f)
caption=ref.copy();caption.data=ref.data.copy();caption.animation_data_clear();caption.data.body=title;caption.name='Tour chapter '+title;caption_col.objects.link(caption)
half=36/lens;caption.location=(-half*.91,half*9/16*.84,-2)
for f,sz in [(1,0),(start-1,0),(start,1),(end,1),(end+1,0)]:
caption.scale=(sz,sz,sz);caption.keyframe_insert('scale',frame=f)
s.timeline_markers.new(title,frame=start);chapters.append({'title':title,'start':start,'end':end,'seconds':5})
for title in caption_col.objects:
if title.type=='FONT' and title.animation_data:
for layer in title.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for fc in bag.fcurves:
if fc.data_path=='scale':
for k in fc.keyframe_points:k.interpolation='CONSTANT'
s.frame_end=3600;s.frame_set(3481);s.camera=cam
json.dump(chapters,open(OUT+'/tour-chapters-daily-life.json','w'),indent=2)
s['Daily life additions']='Covered exercise area, ten-place outdoor table and tea counter, folding table tennis, unbuilt basketball reserve, garden walking loop and quiet bench, breakfast counter, pantry/freezer and harvest bench.'
s['Training design status']='Concept geometry only: dedicated exercise frame requires appropriate manufacturer rating/engineering and equipment clearances; future basketball rectangle is a reserved area, not a built level court.'
report={'training_area':1,'pullup_bar':1,'wooden_rings':2,'adjustable_dumbbells':2,'adjustable_bench':1,'exercise_mats':2,'outdoor_seats':10,'tea_counter':1,'folding_table_tennis':1,'basketball_status':'Future reserve only','walking_loop':True,'quiet_bench':1,'breakfast_counter':1,'pantry':1,'freezer':1,'harvest_bench':1,'tour_chapters':30,'tour_seconds':150,'kitchen':kitchen_checks}
json.dump(report,open(OUT+'/daily-life-checks.json','w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-daily-life-tour.blend')
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT':o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=24;s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU'
for fname,frame in [('training-area.png',2920),('outdoor-gathering.png',3035),('kitchen-copper-marble.png',3520)]:
s.frame_set(frame);s.render.filepath=OUT+'/'+fname;bpy.ops.render.render(write_still=True)
s.frame_set(3300);cam.animation_data_clear();cam.data.animation_data_clear();cam.location=(48,55,48);cam.rotation_euler=(Vector((2,-8,0))-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.lens=37;s.render.filepath=OUT+'/daily-life-overview.png';bpy.ops.render.render(write_still=True)
print('DAILY_LIFE_READY '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,64 @@
from pathlib import Path
from html import escape
import json
O=Path(__file__).parent
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1120" viewBox="0 0 1400 1120"><rect width="1400" height="1120" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}</style><text x="60" y="60" font-size="32" font-weight="600">A home for movement, connection and quiet.</text><text x="60" y="95" font-size="17">SAMFORD VALLEY / Updated concept plan · 10 September 2026 · Version 003</text>']
def pt(x,y):return (630+13*x,470-13*y)
def label(x,y,lines,size=12):
X,Y=pt(x,y)
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
def rect(cx,cy,w,d,fill,stroke='#526154',dash=''):
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*13}" height="{d*13}" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
def line(points,color,width):
pts=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
layout=json.loads((O/'daily-life-layout.json').read_text())
line(layout['walking_loop'],'#adc49a',14)
line([(14,1),(13,4),(13,7),(6,7)],'#adc49a',14)
line([(-1,24),(-7,21),(-14,18)],'#adc49a',14)
label(9,27.4,['SHADED WALKING LOOP'],12)
line([(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)],'#c9bca1',52)
rect(-1,-12.5,12,6,'#c9bca1',stroke='#c9bca1')
line([(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],'#d5cdbb',12)
line([(-3,5.4),(-3,9),(-11,9)],'#b5a88c',16)
rect(0,0,12,6,'#ddc8a9');label(1,.7,['SHARED HOME','Kitchen · dining'],10)
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['VERANDA'],11)
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.8,1.5,['Short link'],10)
rect(-11.8,-4.9,8,5,'#ddc8a9');label(-11.8,-4.5,['PARENTS','Bedroom + bath'],11)
rect(-11,12,4.5,4.8,'#c9c1a5');label(-11,12.4,['QUIET','ROOM'],11)
X,Y=pt(-11,12);p.append(f'<circle cx="{X}" cy="{Y+24}" r="5" fill="#626257"/>')
for i,(x,y) in enumerate([(-3,17),(7,15)]):
rect(x,y,4.5,4.8,'#e6e8de',stroke='#98a394',dash='stroke-dasharray="5 5"');label(x,y+.6,['FUTURE',f'SPACE {i+1}'],11)
label(2,21.4,['Faint outlines only / build when needed'],12)
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="29" ry="25" fill="#bc9d75" stroke="#526154" stroke-width="2"/>');label(-23,2,['SAUNA'],10)
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7.5,['Outdoor shower'],11)
for x in [-23.95,-26.25]:
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="13" fill="#6b7f61"/>')
label(-25,-10.5,['Rainwater tanks'],11)
rect(-3,-19,8,7,'#bcb9ac');label(-3,-18.6,['GARAGE','8 × 7 m'],12)
rect(-4.7,-19.1,1.9,4.35,'#788c83');rect(-.47,-21.5,2.5,.76,'#ab9673')
label(-3,-24,['One car · workbench · bike racks'],12)
label(20,-25,['4 m gravel access road'],13)
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7.4,['GREENHOUSE'],9)
for x in [23.8,27.5]:
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
label(25.5,7.5,['FOOD GARDEN'],12)
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17.2,['GROUND SOLAR'],10)
label(34,-42,['Site approach'],12)
rect(*layout['training'],'#b9c8ad');label(-12.2,-16.8,['COVERED','TRAINING','4.8 × 5.4 m'],10)
line([(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],'#d5cdbb',12)
label(-15.8,-23.2,['Bar · rings · weights','Bench · movable mats'],11)
rect(*layout['outdoor_table'],'#d9c19b');rect(10,3.5,3.2,1.03,'#ab8962')
rect(10,4.48,2.95,.43,'#bda078');rect(10,2.52,2.95,.43,'#bda078')
rect(6.5,4.2,1,1.5,'#c8d0b8');label(10,9,['OUTDOOR TABLE','10 places + tea counter'],11)
rect(*layout['table_tennis'],'#ced8c4');rect(11,-8.5,1.525,2.74,'#608570')
label(11,-5.9,['TABLE TENNIS'],10);label(11,-12.8,['Folding table / level deck'],10)
line([(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],'#d5cdbb',12)
rect(*layout['future_basketball'],'#eceee4',stroke='#81947c',dash='stroke-dasharray="7 7"')
label(13,-35,['FUTURE','BASKETBALL','10 × 10 m reserve'],11)
label(13,-43,['Unbuilt / separate from driveway'],11)
rect(30,10.9,1.8,.45,'#a58a66');label(32,14,['QUIET BENCH'],11)
rect(29.8,1.5,.78,2.05,'#a58a66');label(35,2.4,['HARVEST','BENCH'],11)
label(4,-4.3,['Breakfast · pantry · freezer'],10)
rect(-3.8,.1,2.2,.95,'#f4f1e9')
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="70" y1="970" x2="200" y2="970" stroke="#263a34" stroke-width="4"/><text x="70" y="999" font-size="13">10 metres</text>','<line x1="60" y1="1040" x2="1340" y2="1040" stroke="#bdc3b6"/><text x="60" y="1070" font-size="15">The Blender presentation fades one facade of each building so you can see inside. Walls remain in the model.</text>','<text x="60" y="1096" font-size="13">Concept layout only. The road approach is illustrative; no surveyed property boundary or approved access is implied.</text></svg>'])
(O/'retreat-garage-plan.svg').write_text(''.join(p))

View File

@@ -0,0 +1,127 @@
# Executed by add_daily_life.py inside the scene's construction context.
# Replace the old simplified kitchen, retaining its position and the shared room.
old_prefixes=('Simple timber kitchen cabinet','Flat cabinet pull','Continuous kitchen worktop','Stainless sink','Exposed kitchen tap','Tap spout','Induction cooktop','Open timber kitchen shelf','Pantry ceramic jar')
for o in list(bpy.data.objects):
if o.name.startswith(old_prefixes):bpy.data.objects.remove(o,do_unlink=True)
# Pale marble: distorted fine mineral veins, soft honed reflections.
marble=mat('Honed natural marble - ivory and warm grey veins',(.78,.76,.70),.26)
ns=marble.node_tree.nodes;ls=marble.node_tree.links;bs=ns.get('Principled BSDF')
coord=ns.new('ShaderNodeTexCoord');noise=ns.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=2.8;noise.inputs['Detail'].default_value=5
ls.new(coord.outputs['Generated'],noise.inputs['Vector'])
scale=ns.new('ShaderNodeVectorMath');scale.operation='SCALE';scale.inputs[3].default_value=.33;ls.new(noise.outputs['Color'],scale.inputs[0])
add=ns.new('ShaderNodeVectorMath');add.operation='ADD';ls.new(coord.outputs['Generated'],add.inputs[0]);ls.new(scale.outputs[0],add.inputs[1])
wave=ns.new('ShaderNodeTexWave');wave.wave_type='BANDS';wave.bands_direction='DIAGONAL';wave.inputs['Scale'].default_value=3.7;wave.inputs['Distortion'].default_value=6;wave.inputs['Detail'].default_value=4;ls.new(add.outputs[0],wave.inputs['Vector'])
ramp=ns.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].position=.055;ramp.color_ramp.elements[0].color=(.20,.21,.20,1);ramp.color_ramp.elements[1].position=.14;ramp.color_ramp.elements[1].color=(.81,.79,.73,1)
ls.new(wave.outputs['Color'],ramp.inputs[0]);ls.new(ramp.outputs[0],bs.inputs['Base Color'])
fine=ns.new('ShaderNodeTexNoise');fine.inputs['Scale'].default_value=170;ls.new(coord.outputs['Generated'],fine.inputs['Vector']);bump=ns.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.10;bump.inputs['Distance'].default_value=.0005;ls.new(fine.outputs['Fac'],bump.inputs['Height']);ls.new(bump.outputs[0],bs.inputs['Normal'])
copperpot=textured('Copper cookware - brushed warm patina',(.32,.10,.035),(.72,.34,.13),(75,75,8),.26,.0006);copperpot.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.94
stainless=mat('Cookware food-contact stainless lining',(.55,.59,.61),.23,.96)
ovenblack=mat('Oven and induction reflective smoked glass',(.009,.016,.019),.12,.25)
mark=mat('Induction subtle etched markings',(.36,.42,.42),.45)
display=mat('Appliance soft white display',(.72,.82,.75),.3);display.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.65,.85,.74,1);display.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=.6
# Run of detailed cabinets, including a 600 mm oven bay.
for i,(x,w) in enumerate([(-5.15,.72),(-4.32,.90),(-3.36,.96),(-2.38,.96),(-1.49,.78),(-.88,.42)]):
box('Kitchen timber cabinet carcass',(x,-2.53,F+.45),(w,.64,.88),wood,.008)
box('Kitchen recessed dark plinth',(x,-2.53,F+.075),(w-.06,.55,.13),steel,.006)
if i==4:continue
for row,h in enumerate([.29,.56,.79]):
box('Kitchen flush timber drawer',(x,-2.194,F+h),(w-.018,.032,.22 if row<2 else .19),wood,.006)
rod('Kitchen brushed drawer pull',(x-.14,-2.162,F+h+.065),(x+.14,-2.162,F+h+.065),.009,metal,sides=12)
# Worktop parts leave a real sink opening at x=-4.32.
for x,w in [(-5.15,.80),(-2.405,3.21)]:box('Kitchen honed marble rear worktop',(x,-2.52,F+.925),(w,.75,.045),marble,.008)
for y,d in [(-2.80,.19),(-2.225,.16)]:box('Sink cutout marble edge',(-4.32,y,F+.925),(.86,d,.045),marble,.005)
box('Kitchen marble low splashback',(-3.1,-2.905,F+1.09),(4.95,.025,.28),marble,.006)
# Recessed sink basin with separate bottom and four walls.
box('Sink recessed basin floor',(-4.32,-2.5,F+.73),(.66,.40,.025),stainless,.015)
for x in [-4.66,-3.98]:box('Sink basin side',(x,-2.5,F+.825),(.025,.42,.20),stainless,.009)
for y in [-2.715,-2.285]:box('Sink basin end',(-4.32,y,F+.825),(.70,.025,.20),stainless,.009)
rod('Sink drain',(-4.32,-2.5,F+.744),(-4.32,-2.5,F+.750),.044,steel,sides=32)
# Curved copper tap, exposed and replaceable.
pts=[(-4.32,-2.8,F+.95),(-4.32,-2.8,F+1.26)]
for i in range(17):
a=pi-pi*i/16;pts.append((-4.32,-2.69+.11*cos(a),F+1.26+.11*sin(a)))
for i,(a,b) in enumerate(zip(pts,pts[1:])):rod('Copper mixer tap '+str(i),a,b,.014,copperpot,sides=16)
rod('Tap outlet',pts[-1],(-4.32,-2.58,F+1.20),.014,copperpot,sides=16)
rod('Tap lever',(-4.20,-2.78,F+.95),(-4.20,-2.78,F+1.07),.01,stainless,sides=12)
# Built-in oven, facing the living room (north).
ox=-1.49
box('OVEN - brushed steel surround',(ox,-2.177,F+.49),(.62,.04,.68),metal,.013)
box('OVEN - insulated glass door',(ox,-2.144,F+.43),(.555,.032,.44),ovenblack,.015)
box('Oven inner viewing panel',(ox,-2.125,F+.435),(.435,.006,.285),mat('Oven glass subtle interior',(.025,.035,.035),.18),.018)
for z in [F+.37,F+.49]:box('Oven internal rack hint',(ox,-2.120,z),(.39,.004,.008),metal)
rod('Oven tubular handle',(ox-.23,-2.085,F+.663),(ox+.23,-2.085,F+.663),.018,metal,sides=20)
for x in [ox-.22,ox+.22]:rod('Oven handle standoff',(x,-2.14,F+.663),(x,-2.085,F+.663),.012,metal,sides=16)
box('Oven control fascia',(ox,-2.142,F+.779),(.57,.028,.115),ovenblack,.007)
for x in [ox-.205,ox+.205]:rod('Oven rotary dial',(x,-2.13,F+.78),(x,-2.097,F+.78),.027,metal,sides=32)
box('Oven display',(ox,-2.121,F+.78),(.14,.004,.036),display,.002)
for i in range(9):box('Oven bottom ventilation slot',(ox-.24+i*.06,-2.15,F+.17),(.035,.009,.011),black,.002)
# Four-zone induction glass and etched circles.
box('INDUCTION - four zone glass cooktop',(-1.49,-2.51,F+.961),(.70,.53,.018),ovenblack,.009)
def horizontal_ring(name,x,y,z,r,t,material):
ob=ring(name,(0,0,0),r,t,material);ob.rotation_euler.x=pi/2;ob.location=(x,y,z);return ob
for x,y,r in [(-1.67,-2.64,.105),(-1.30,-2.64,.086),(-1.68,-2.40,.082),(-1.32,-2.40,.103)]:
horizontal_ring('Induction etched cooking zone',x,y,F+.973,r,.0012,mark)
for i in range(7):box('Induction touch slider',(-1.65+i*.05,-2.277,F+.972),(.019,.009,.002),mark,.001)
# Slim extraction canopy with visible duct; no hidden construction language.
box('Kitchen extraction hood',(-1.49,-2.49,F+1.68),(.82,.54,.075),metal,.01)
for x in [-1.74,-1.24]:box('Hood warm task light',(x,-2.35,F+1.639),(.08,.12,.003),cream)
rod('Exposed kitchen extraction duct',(-1.49,-2.60,F+1.71),(-1.49,-2.60,F+2.33),.105,metal,sides=48)
# Timber shelving away from hob and exposed copper cookware rail.
box('Kitchen upper hardwood shelf',(-3.96,-2.72,F+1.84),(2.77,.32,.05),wood,.008)
rod('Copper cookware hanging rail',(-5.1,-2.57,F+1.52),(-2.75,-2.57,F+1.52),.014,copperpot,sides=20)
for x in [-5.04,-2.81]:rod('Cookware rail wall bracket',(x,-2.89,F+1.52),(x,-2.57,F+1.52),.012,metal,sides=16)
# Hollow copper vessels with stainless lining; induction-compatible base concept.
def vessel(n,x,y,z,r,h):
N=64;profile=[(0,0),(r*.83,0),(r,.025),(r,h),(r-.012,h+.003),(r-.016,.036),(0,.036)]
vs=[(x+rr*cos(i*2*pi/N),y+rr*sin(i*2*pi/N),z+zz) for rr,zz in profile for i in range(N)]
fs=[]
for j in range(len(profile)-1):
for i in range(N):fs.append((j*N+i,j*N+(i+1)%N,(j+1)*N+(i+1)%N,(j+1)*N+i))
ob=mesh(n,vs,fs,copperpot);ob.data.materials.append(stainless)
for p in ob.data.polygons:p.use_smooth=True;p.material_index=1 if p.index//N>=3 else 0
rod(n+' induction compatible base concept',(x,y,z-.003),(x,y,z+.004),r*.85,stainless,sides=64)
horizontal_ring(n+' rolled rim',x,y,z+h,r-.005,.006,stainless)
return ob
vessel('COPPER STOCKPOT',-2.91,-2.50,F+.954,.18,.235)
for x in [-3.13,-2.69]:
rod('Stockpot handle',(x,-2.57,F+1.12),(x,-2.43,F+1.12),.012,metal,sides=16)
for y in [-2.57,-2.43]:rod('Stockpot handle fixing',(x,y,F+1.12),(x+(.055 if x<-2.9 else -.055),y,F+1.12),.009,metal,sides=12)
vessel('COPPER SAUCEPAN',-1.32,-2.40,F+.977,.09,.09)
rod('Saucepan handle',(-1.22,-2.40,F+1.05),(-.95,-2.40,F+1.09),.012,metal,sides=20)
vessel('COPPER FRYING PAN',-3.45,.02,F+.965,.155,.037)
rod('Frying pan long handle',(-3.29,.02,F+1.0),(-3.0,.02,F+1.03),.013,metal,sides=20)
# Marble-topped island: central prep surface clear of the dining table.
ix,iy=-3.8,.10
box('ISLAND - warm timber cabinet body',(ix,iy,F+.44),(2.05,.83,.88),wood,.012)
box('ISLAND - recessed plinth',(ix,iy,F+.06),(1.93,.73,.12),steel,.007)
box('ISLAND - honed marble countertop',(ix,iy,F+.94),(2.2,.95,.06),marble,.012)
for x in [ix-.68,ix,ix+.68]:
for z in [.30,.69]:
box('Island drawer front',(x,iy-.425,F+z),(.657,.028,.36),wood,.006)
rod('Island drawer pull',(x-.14,iy-.448,F+z+.09),(x+.14,iy-.448,F+z+.09),.009,metal,sides=16)
for i in range(29):box('Island north face timber slat',(ix-.98+i*.07,iy+.425,F+.49),(.042,.023,.75),woodz,.004)
box('Island end grain chopping board',(-4.37,.14,F+.985),(.45,.34,.03),wood,.015)
# Tea crockery and pantry objects add scale without filling the work surface.
for x in [-5.03,-4.67,-4.30]:
vessel('Shelf ceramic storage vessel',x,-2.71,F+1.87,.085,.18)
for ob in list(col.objects):
if ob.name.startswith('Shelf ceramic storage vessel') and ob.type=='MESH':
for slot in range(len(ob.data.materials)):ob.data.materials[slot]=cream
# Rebuild only the old furniture proxy so replaced kitchen blocks disappear in fast view.
furniture=bpy.data.collections.get('10 Furniture');fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
oldproxy=bpy.data.objects.get('FAST | 10 Furniture')
if oldproxy and furniture:
vs=[];fs=[];indices=[];materials=[];mapping={}
for ob in furniture.objects:
if ob.type!='MESH' or ob.hide_render:continue
remap=[]
for m in ob.data.materials:
if m not in mapping:mapping[m]=len(materials);materials.append(m)
remap.append(mapping[m])
world=Matrix.LocRotScale(ob.location.copy(),ob.rotation_euler.to_quaternion(),ob.scale.copy());off=len(vs);vs.extend(tuple(world@v.co) for v in ob.data.vertices)
for poly in ob.data.polygons:fs.append(tuple(off+j for j in poly.vertices));indices.append(remap[poly.material_index])
me=bpy.data.meshes.new('Furniture after kitchen replacement');me.from_pydata(vs,[],fs);me.update()
for m in materials:me.materials.append(m)
me.polygons.foreach_set('material_index',indices);oldproxy.data=me
kitchen_checks={'oven':1,'induction_zones':4,'copper_stockpot':1,'copper_saucepan':1,'copper_frying_pan':1,'marble_island_m':[2.2,.95],'island_rear_work_aisle_m':1.67,'old_simplified_kitchen_removed':True,'fast_furniture_refreshed':bool(oldproxy)}
s['Kitchen design']='Honed marble island, detailed timber drawers, oven, four-zone induction, extraction, hollow copper cookware with stainless food-contact lining and an induction-compatible base concept.'

View File

@@ -0,0 +1,19 @@
import bpy,json,os
s=bpy.context.scene;out=os.path.dirname(__file__)
chapters=json.load(open(out+'/tour-chapters-daily-life.json'))
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
issues=[];captions=[]
for c in chapters:
frame=c.get('start',c.get('start_frame',1))+30;s.frame_set(frame);bpy.context.view_layer.update()
shown=[o.data.body for o in fonts if o.scale.length>.5]
captions.append({'frame':frame,'titles':shown})
if len(shown)!=1:issues.append({'frame':frame,'titles':shown})
required=['OVEN - insulated glass door','INDUCTION - four zone glass cooktop','ISLAND - honed marble countertop','COPPER STOCKPOT','COPPER SAUCEPAN','COPPER FRYING PAN','PULL UP BAR - dedicated steel frame','OUTDOOR TABLE - ten place timber top','SHADED WALKING LOOP - garden and quiet room','HARVEST BENCH - garden sorting surface','PANTRY - tall timber cabinet','FREEZER - compact upright']
missing=[n for n in required if n not in bpy.data.objects]
report={'chapters':len(chapters),'end_frame':s.frame_end,'fps':s.render.fps,'missing':missing,'caption_issues':issues,'caption_samples':captions,'render_engine':s.render.engine,'render_size':[s.render.resolution_x,s.render.resolution_y],'detail_render_enabled':not bpy.data.collections['DETAIL - full geometry for renders'].hide_render,'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render,'training_steps':sum(o.name.startswith('Training access step') for o in s.objects),'play_steps':sum(o.name.startswith('Play deck access step') for o in s.objects)}
json.dump(report,open(out+'/daily-life-verification.json','w'),indent=2)
assert not missing and not issues,report
assert report['chapters']==30 and s.frame_end==3600 and s.render.fps==24
assert report['detail_render_enabled'] and report['fast_excluded_from_render']
assert report['training_steps']==3 and report['play_steps']==3
print('VERIFIED '+json.dumps({k:v for k,v in report.items() if k!='caption_samples'}),flush=True)

View File

@@ -0,0 +1,182 @@
[
{
"title": "01 / THE SPACIOUS HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / FUTURE SPACES - BUILD LATER",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / A HOME THAT CAN CHANGE",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
},
{
"title": "25 / TRAINING & STRENGTH",
"start": 2881,
"end": 3000,
"seconds": 5
},
{
"title": "26 / OUTDOOR TABLE & MATCHA",
"start": 3001,
"end": 3120,
"seconds": 5
},
{
"title": "27 / PLAY & FUTURE BASKETBALL",
"start": 3121,
"end": 3240,
"seconds": 5
},
{
"title": "28 / WALKING & FOOD GARDEN",
"start": 3241,
"end": 3360,
"seconds": 5
},
{
"title": "29 / EASY EVERYDAY FOOD",
"start": 3361,
"end": 3480,
"seconds": 5
},
{
"title": "30 / COPPER & MARBLE KITCHEN",
"start": 3481,
"end": 3600,
"seconds": 5
}
]

View File

@@ -0,0 +1,182 @@
[
{
"title": "01 / ONE CONNECTED GARDEN HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / BALANCED FUTURE PAVILIONS",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / RECTANGLES AND GARDEN CURVES",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
},
{
"title": "25 / TRAINING & STRENGTH",
"start": 2881,
"end": 3000,
"seconds": 5
},
{
"title": "26 / OUTDOOR TABLE & MATCHA",
"start": 3001,
"end": 3120,
"seconds": 5
},
{
"title": "27 / FLOWERING GARDEN - ROOM TO BREATHE",
"start": 3121,
"end": 3240,
"seconds": 5
},
{
"title": "28 / FLOWING WALKS AND GARDEN COURT",
"start": 3241,
"end": 3360,
"seconds": 5
},
{
"title": "29 / EASY EVERYDAY FOOD",
"start": 3361,
"end": 3480,
"seconds": 5
},
{
"title": "30 / COPPER & MARBLE KITCHEN",
"start": 3481,
"end": 3600,
"seconds": 5
}
]

View File

@@ -0,0 +1,146 @@
[
{
"title": "01 / THE SPACIOUS HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / FUTURE SPACES - BUILD LATER",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / A HOME THAT CAN CHANGE",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
}
]

View File

@@ -0,0 +1,9 @@
# Initial project milestone — 10 September 2026
- Preserve the full Blender homestead with main kitchen/dining/living pavilion, adjacent parents’ bedroom, meditation room, detached sauna and shower, rainwater tanks, solar, greenhouse, garden beds, road and Tesla garage/workstation/bike storage.
- Keep future children’s spaces as ghost outlines. Only the meditation room is built in the current children’s-module area.
- Add the reading corner with two filled bookshelf bays and one chair.
- Preserve the first actual Unreal Engine meditation-room level and still render.
- Establish space for future surveyed terrain, LiDAR, coordinated plans and a whole-property Unreal walkthrough.
Next decisions: actual property and survey; room sizes and circulation after testing daily routines; how future additions connect; material assemblies; energy/water demand; and professional design coordination. No land purchase, site survey, regulatory review, engineering or capacity sizing is represented as complete.

View File

@@ -0,0 +1,5 @@
# Bedroom sleep setup
Marcus requested Eight Sleep, blackout curtains and air conditioning. Added a fitted cooling cover and ventilated bedside hub, lined curtains with open/closed states, and indoor/outdoor AC units. Kept natural timber and linen finishes. Updated chapter 08 camera framing and added a bedroom render. No changes to the Unreal meditation room.
Product reference: https://www.eightsleep.com/product/pod-cover

View File

@@ -0,0 +1,7 @@
# Daily life, movement and kitchen
Marcus approved training equipment, outdoor meals for 8–10 people, table tennis with a future basketball reserve, a shaded garden walk and convenient food preparation/storage. He then requested higher kitchen fidelity: oven, induction, copper pots and pans and a marble countertop island.
Kept the narrow verandah clear by adding an adjoining gathering terrace. Training and table tennis have separate decks with entry steps. No new enclosed child modules were built. Future building outlines remain visible. Added six tour chapters and regenerated the site plan from matching layout coordinates. Retained lightweight viewport geometry.
Actual Blender renders: training-area.png, outdoor-gathering.png, daily-life-overview.png, kitchen-copper-marble.png. Saved-file verification checks required objects, access steps and a single visible caption in all 30 chapters.

View File

@@ -0,0 +1,7 @@
# Garden-centred composition
Marcus rejected the separate table-tennis area and requested cohesive geometry, an element of symmetry, smooth curves connecting buildings, and more planting/flowers. He also requested a private repository on git.rehbock.xyz containing the context and Blender file, created through SSH on rehbock.xyz.
Current axes: main pavilion and garage x=0; parents and meditation x=-11.8; east future reserve x=11.8; northern reserve x=0. Service row y=-19 holds training, garage and ground solar. The buildings retain simple rectangular forms; the garden court, covered timber breezeway, pedestrian routes and vehicle approach provide the curves. There are 13 flowering beds, 221 planted clumps and eight shrub groups. Table-tennis geometry is absent.
Validated saved-file objects, subject positions, preserved kitchen/tanks, 30 captioned chapters, full 2560×1440 render settings and fast-view render exclusion. Inspected actual Cycles images. This remains an unsurveyed concept; symmetry and paths are not an engineering or planning approval.

77
docs/PROJECT-CONTEXT.md Normal file
View File

@@ -0,0 +1,77 @@
# Samford Valley home — project context
This is Marcus Rehbock's long-term personal homestead design project. The aim is to refine the design over several years, choose a real site, add survey and LiDAR information, coordinate construction plans, and eventually create a full Unreal Engine 5 walkthrough. The design is a working concept, not an approved or engineered building proposal.
## Current state: version 004, 10 September 2026
Open `blender/samford-solarpunk-garden-flow-tour.blend` in Blender 5.2.1. This is the current model. It includes the detailed kitchen and bedroom upgrades, a garden-centred arrangement, 13 flowering beds, and a 30-chapter camera tour lasting 2 minutes 30 seconds at 24 fps. The former table-tennis platform and its equipment have been removed. Earlier dated scene files are historical versions.
The latest plan is `plans/concept/garden-flow-plan.svg`. It is generated from the same `garden-flow-layout.json` coordinates used by the Blender revision. Current images are `renders/garden-flow-overview.png`, `garden-court.png`, `flowering-kitchen-garden.png`, `garden-flow-dining.png` and `garden-flow-training.png`.
## Philosophy and values
Marcus wants a peaceful, modest, natural home that supports family, strength training, meditation, sleep, books, shared meals and growing food. He described the overall feeling as **solarpunk**. The house should sit in its subtropical hillside landscape rather than dominate it.
The initial inspiration is Derek Sivers' **Building without predicting**: https://sive.rs/fit . Start with simple, well-insulated rectangular modules; live in them and adapt rather than predicting everything in advance. Stewart Brand's phrase used in the brief is: “All buildings are predictions. All predictions are wrong.” Keep services and structural connections exposed and replaceable. Future additions must be visibly possible, not presented as already built.
Marcus asked for recommendations based on https://blog.rehbock.xyz . The resulting approved additions support repeated themes of strength training/BJJ, shared coffee and meals, informal social play, quiet bush walking, reading and easy everyday food routines. References: https://blog.rehbock.xyz/3/ , https://blog.rehbock.xyz/4/ , https://blog.rehbock.xyz/5/ . These are design motivations, not medical claims about materials regulating the nervous system.
## Buildings and programme
- **Main pavilion:** approximately 12 × 6 m, long axis east–west, single-storey, one open kitchen/dining/living room. North-facing openable timber-framed sliding doors and a 2.4 m deep verandah. Initial mono-pitch roof brief: 3.4 m north to 2.7 m south. Keep the main shared room central.
- **Parents' wing:** approximately 8 × 5 m, private enclosed bedroom and bathroom adjacent to the main pavilion via a short corridor. Current centre is (-11.8, -4.9). The bedroom is its own enclosed room; it is not part of the open living room. The bathroom is accessed via the small lobby.
- **Meditation room:** one small enclosed pavilion, 4.5 × 4.8 m, with timber, soft clay, a meditation cushion, simple mat, candles and a low bench. Current centre (-11.8, 12), aligned with the parents' wing. It may change use later.
- **Future rooms:** two faint unbuilt pavilion outlines, now centred at (11.8, 12) and (0, 24). The east reserve balances the meditation pavilion around the garden court. Earlier solid children's rooms were explicitly rejected. Do not reinstate them without a new request.
- **Sauna and shower:** detached Finnish-style oval timber sauna, now centred at (-23, 4.2), with separate outdoor shower and stone terrace. It is a sauna, not a detached toilet. The bathroom remains in the parents' wing.
- **Garage:** approximately 8 × 7 m, one white Tesla Model Y-inspired vehicle, workstation/workbench, tool storage, bicycle racks, EV charging and rooftop solar. Centre (0, -19), aligned with the main pavilion. Retain road access and turning space.
- **Training:** covered timber deck centred at (-9.4, -19), with a separate steel exercise frame, pull-up bar, wooden rings, two adjustable dumbbells, adjustable bench and movable mats. The intended real frame must have an appropriate manufacturer/engineer load rating; this concept geometry is not a certification.
- **Outdoor meals:** terrace adjoining the east end of the verandah, table for ten with benches and end chairs, pergola shade and a tea/matcha preparation counter.
## Kitchen, sleep and technology
The kitchen now contains detailed timber cabinetry/drawers, an oven, four-zone induction cooktop, exposed extraction duct, recessed sink and copper tap, copper pots/pan with stainless food-contact surfaces and a compatible-base concept, and a 2.2 × 0.95 m honed marble-topped island. The approximate island-to-rear-counter aisle is 1.7 m. A breakfast-prep counter, pantry, compact upright freezer and outdoor harvest bench support convenient food routines.
The main living room has a two-bay timber bookcase with 163 books, a reading chair, side table and reading light.
The bed has an Eight Sleep Pod-style cover and bedside cooling hub, lined blackout curtains and split-system air conditioning with an outdoor condenser. These are indicative product forms, not final selections. To close the curtains, enable the detailed scene, select each BLACKOUT CURTAIN, and set the `Closed - blackout` shape key to 1; 0 is open. The fast display copy shows the open state.
Starlink is included. One local-AI server sits in a ventilated cabinet in the main house, without a separate server room. Marcus asked for enough computer capacity to run a “64GB model” locally. Existing model naming targets 128–256 GB system memory; exact model size, quantisation, accelerators, power, cooling and hardware selection remain unresolved.
## Landscape, circulation and symmetry
Version 004 responds to Marcus's request for a more cohesive architectural composition, an element of symmetry, and smooth curves in the relationships between buildings. Rectangular pavilions retain a common timber/mono-pitch vocabulary. A central garden axis links the house, oval garden court, northern future reserve and garage. The west meditation pavilion and east future reserve balance one another. Training and ground solar align on either side of the garage.
The garden court has a continuous oval gravel walk, low central groundcover and paired flower beds. A curved covered timber breezeway connects the verandah to the meditation entry. Smooth gravel paths connect dining, the food garden, the quiet bench, sauna, tanks and arrival. The car route is distinct from the pedestrian garden routes. Thirteen planted beds contain illustrative native grasses, flowering perennials and bottlebrush-inspired shrubs. Two concept eucalyptus groups were relocated clear of the moved solar array and driveway.
The table-tennis table **and entire dedicated deck** were removed at Marcus's request because they did not feel cohesive. Their former area is now a flowering kitchen garden. A separate **future** 10 × 10 m basketball reserve remains south of the service zone; it has neither a court surface nor a working hoop. The user did not ask to remove this reserve.
Keep north-facing views, sun access and air movement in mind. Do not treat symmetry as a reason to block the verandah or force every room into identical dimensions. No property boundary, site survey, slope engineering, flood assessment, bushfire assessment or approved vehicle access has been supplied. The landscape and roads are illustrative.
Design references consulted in this revision:
- https://www.yourhome.gov.au/passive-design/orientation
- https://www.yourhome.gov.au/passive-design/shading
- https://www.brisbane.qld.gov.au/about-council/your-brisbane/environment-and-sustainability/gardening/best-plants-for-brisbane-gardens
## Materials
Marcus prefers “warm timber, soft clay walls, selective hemp insulation, stone underfoot where appropriate, and honest steel connections.” Timber, bamboo ceiling lining, clay/mineral finishes, exposed rafters and warm concrete/stone remain central. The initial brief included spotted-gum posts and weatherboards, aged silver-grey timber, warm oxide concrete, and light Colorbond roofing. Natural materials are preferred, with steel and other construction materials used where useful. He asked about cob, mud, bamboo and low-toxicity materials, but did not commit to a complete cob or earth-wall building system. Avoid unsupported claims that a material is non-toxic in all forms or clinically regulates the nervous system.
## Energy and homesteading
Keep rooftop solar, a separate ground array (current conceptual footprint centred at 9.4, -19), off-grid energy storage, two dark-green cylindrical rainwater tanks, greenhouse, six raised vegetable beds, fruit trees and garden harvest bench. Product capacities, structural design, solar yield, battery autonomy, water balance, wastewater treatment and food-production targets have not been engineered.
## Blender and Unreal state
Blender has full editable geometry and Cycles materials. Saved render settings are 2560 × 1440 at 100%; web preview renders are made at 60% (1536 × 864). The user previously reported low playback FPS. `00 FAST TOUR - lightweight viewport` is a static simplified display; `DETAIL - full geometry for renders` is disabled only in the viewport. Cycles renders exclude FAST and use DETAIL automatically. For detailed editing, enable DETAIL and disable FAST. After moving or removing geometry, refresh the affected fast copies as well.
The tour contains 30 five-second chapters. Space plays/pauses with the pointer over the 3D view; Shift+Left Arrow returns to frame 1. The current scene opens at the overview. Update camera positions and captions whenever a subject moves.
The **actual Unreal work covers only the meditation room**. `unreal/SamfordMeditation/SamfordMeditation.uproject` is the UE 5.8.2 project, with room geometry, materials, light and camera. It is not a complete Unreal homestead. FBX/GLB exports and textures are retained. Camera animation instructions are in `walkthroughs/UNREAL-CAMERA-ANIMATION.md`. A complete interactive home/site walkthrough remains future work.
## Storage and continuation
Private repository: https://git.rehbock.xyz/marcus/samford-valley-home . Server working archive/repository: `/home/marcus/projects/samford-valley-home`. Public gallery: https://rehbock.xyz/projects/samford-valley-home/ . Only selected images, public descriptions and a concept SVG are in the public website; working assets belong in the private project.
The repository includes Blender models, renders, plans, design context, revision records, source scripts, Unreal assets and reserved folders for survey/LiDAR/construction information. Dated compressed snapshots remain in the server archive and are excluded from Git to avoid duplicating the entire repository in each commit. Git LFS is disabled on the existing Gitea installation; current assets are stored as ordinary Git binaries. Plan large-file/object storage before adding multi-gigabyte LiDAR or video datasets. No new off-site backup service has been configured.
Read `REQUEST-HISTORY.md`, the decisions folder and `AGENTS.md` before making further changes. Do not mistake historical renders or earlier briefs for the current approved layout.

38
docs/REQUEST-HISTORY.md Normal file
View File

@@ -0,0 +1,38 @@
# Request history and decisions
This records the supplied conversation's substantive requests in order. It is a consolidated design record, not a verbatim transcript or a claim that every exploratory question has a final answer. Latest explicit requests take precedence.
1. Build a simple house in Blender on a lush Samford Valley, Brisbane hillside.
2. Detailed initial brief: a 12 × 6 m open living rectangle, 8 × 5 m sleeping rectangle, mono-pitch roofs, exposed spotted gum, full-height sliding doors, covered verandah and breezeway, ghost future office/guest/workshop rectangles, native planting, food beds, rainwater tanks and warm morning light. Cycles at 2560 × 1440. Derek Sivers/Stewart Brand adaptation philosophy.
3. Add rooftop solar and an off-grid array layout, rainwater, greenhouse and garden beds for growing food.
4. Make the house clearly extendible and materials as natural as possible.
5. Explore cob, mud, bamboo and materials associated with calm/low toxicity. No final all-cob system was selected.
6. Add an attractive Finnish-style detached sauna (oval acceptable), outdoor shower, tanks, Starlink and one local AI server without a server room. Preferred palette: warm timber, soft clay walls, selective hemp insulation, stone and honest steel connections.
7. Explain Blender navigation and add a Play-button camera tour. Spread the buildings out; avoid a cramped layout. Clarify the adjoining building's use.
8. Give the bedroom its own enclosed room adjacent to central kitchen/dining, via a short corridor.
9. Explore land area for a homestead with a spouse and three children. No surveyed parcel or definitive hectare requirement was adopted.
10. Explore separate enclosed, customisable spaces for each child, inspired by the modular idea.
11. **Superseding decision:** remove the solid children's rooms and leave future shadows. Keep one small room as a meditation retreat with cushion/candles. Add vehicle road, garage, workstation and bike racks. Use a transparent viewing side on buildings to see interiors.
12. Improve low playback FPS. Keep detailed render geometry but use simplified viewport copies.
13. Make the garage car a Tesla and the overall concept solarpunk.
14. Explore a build-and-contents cost estimate. No verified quantity-surveyor estimate or adopted budget is contained in this record.
15. Scope conversion to Unreal Engine 5; add bookshelves full of books, a reading chair and a simple actual UE meditation-room render. The user installed UE locally and asked about plugins.
16. Put the Blender project and screenshots on the server under Projects. Preserve it over several years for 3D plans, construction-site LiDAR, Blender mockups and a future Unreal walkthrough. Explain Unreal room camera animation.
17. Add the Derek Sivers building article link to the public house project page.
18. Add Eight Sleep, blackout curtains and air conditioning to the bedroom.
19. Review the user's blog/values for further house ideas, including pull-ups and free weights.
20. Approve five additions: covered training with bar/rings/dumbbells/bench/mats; outdoor table for 8–10 and tea/matcha; folding table tennis with a future separate basketball area; shaded garden walking loop ending at a quiet bench; breakfast preparation, pantry/freezer and harvest bench.
21. Upgrade kitchen fidelity with oven, induction, copper pots and pans and a marble countertop island.
22. **Current layout revision:** remove the ping-pong/table-tennis area because it lacks cohesion. Improve geometric relationships, an element of symmetry, smooth curves between buildings, and add more gardens/flowers/plants. Implemented as version 004 with aligned pavilions, garden court, curved paths and covered breezeway. The future basketball reserve was not cancelled.
23. **Current preservation request:** create a repository on git.rehbock.xyz that keeps this context and the Blender file. The user explicitly instructed using SSH into rehbock.xyz to do this. Repository setup uses the server's existing authenticated Git key and private push-to-create capability.
## Milestones
- v001: initial server project archive, public gallery, reading library and actual Unreal meditation study.
- v002: bedroom sleep setup.
- v003: daily-life additions and detailed kitchen; 30-chapter, 2½-minute tour.
- v004: table-tennis area removed; coherent garden-centred layout and flowering landscape; private Git repository and consolidated context.
## Still to decide
Actual property and required area; surveyed levels/boundaries; planning approvals and bushfire/flood response; engineering and detailed accessible circulation; quantities/budget; product selections and energy/water balances; server hardware; final botanical planting plan; large LiDAR storage and independent backups; full Unreal conversion and interactive walkthrough. Preserve these as future work rather than assuming they are completed.

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

View File

@@ -0,0 +1,117 @@
# Samford Solarpunk Homestead — Unreal Engine 5 project scope
Prepared 10 September 2026. Proposal only; no Unreal installation, import or project build has been performed.
## Recommended outcome
An editable UE5 architectural scene and a local Mac application for exploring the current homestead. Keep Blender as the source for building design; use Unreal for real-time presentation. The first release preserves the current layout, natural material palette, solar infrastructure and future expansion footprints.
Assumptions: one local user, keyboard/mouse or trackpad, this Mac as the first target, fixed golden-hour lighting, and no construction-grade or energy simulation. The scope reuses the existing concept models; a commercial-quality replacement vehicle or furniture library is additional art work.
## Existing source and local readiness
- Source: `samford-solarpunk-smooth-tour.blend` in this directory. Its hidden DETAIL collection contains the complete render geometry. Its FAST TOUR collection is a separate, simplified viewport copy. Exporting both would duplicate the scene.
- Existing assets: living/kitchen/dining pavilion; parents' bedroom and bathroom wing; connecting corridor and verandahs; meditation room; two future shadow footprints; sauna and shower; greenhouse and six beds; tanks; roof and ground solar; Starlink and homelab; driveway; garage, Tesla concept car, charging point, workbench and bikes.
- Existing tour: 115 seconds, 23 chapters, animated camera and lens, chapter labels and deliberate cuts.
- Measured Blender display: approximately 4,090 visible objects / 1.91 million faces before optimization, versus 47 objects / 198,000 faces in the fast version. These are Blender polygon counts, not Unreal triangle counts or Unreal performance measurements.
- Local hardware: Apple M5, 24 GiB unified memory. macOS 26.6.2; Xcode 26.6. Approximately 194 GiB free on the workspace volume at inspection.
- No UnrealEditor application was found by Spotlight; no Epic Games installation was present in the standard shared folder or Applications listing. An installation elsewhere remains possible.
- Epic recommends 32 GB or more memory. The current Mac is above the listed minimum but below that recommendation. The pilot must establish a practical visual-quality target on this machine. [Epic Mac requirements](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
## Approaches considered
| Approach | Estimated effort | Result |
|---|---:|---|
| Basic conversion | 12–24 hours | Imported geometry, basic materials, simple fly view. Useful proof, limited polish and interaction. |
| Optimized interactive homestead — recommended | 48–80 hours total | Walk/fly controls, complete tour, cutaways, future footprints, rebuilt materials and vegetation, tested local Mac application. |
| Showcase visualization | 100–180 hours total | Recommended version plus substantial model upgrades, richer botanical assets, more elaborate cinematics and atmosphere. |
These are planning estimates for one experienced Unreal architectural-visualization generalist, not measured execution times or a fixed quote. Installation downloads and first shader compilation add elapsed time. The options are alternative totals, not additive phases.
## First release: user experience
1. A simple start screen: **Walk**, **Fly**, **Take the tour**, **Quality**, **Quit**.
2. Walk at human scale through the home, meditation cabin, garage, sauna and garden. Floors, stairs/ramps and door openings must work physically; presentation-only wall transparency is not sufficient.
3. Fly freely to understand the terrain, solar arrays, road and spatial relationships.
4. Play the 23-chapter tour with pause/resume, restart and chapter selection. Recreate captions as screen UI rather than 3D camera-attached text.
5. In fly/exploration view, **See inside** toggles the presentation wall sections. Walk mode uses coherent physical boundaries and accessible doors.
6. **Future spaces** shows/hides the two existing ghost footprints and their labels. Fully furnished future buildings are outside this first release.
7. **Balanced** and **Fast** presets adjust foliage, lighting, shadows and rendering resolution. Keep the interface small and nontechnical.
The Tesla, solar arrays, server, tanks and sauna are visual assets. Driving, charging simulation, water balance, food production and heat simulation are not included.
## Conversion and scene design
### Asset preparation
Work from a duplicate of the detailed source. Create stable export names and pivots; apply necessary modifiers; check normals, openings, thin surfaces and object scale. Validate that Blender metres become the correct Unreal centimetres and that camera orientation, sun direction and site north remain consistent.
Use FBX mesh exports grouped by building and asset family, with an explicit placement manifest for repeatable imports. Run a small glTF comparison only if FBX material or hierarchy import is troublesome. Keep doors, removable presentation walls, roofs, floors and furniture as logical separate pieces. Merge minor static parts locally and instance repeated posts, panels and vegetation. Avoid importing thousands of unrelated tiny Actors, or merging the entire property into one mesh.
FBX scene import supports geometry and basic materials, but it does not guarantee the source appearance or animated-camera import. This is a conversion workflow, not a one-click format change. [Epic FBX scene import](https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-scene-import-in-unreal-engine)
### Materials and lighting
Create UVs where needed. Bake or rebuild the Blender procedural materials: timber grain, clay, concrete/stone, bamboo, metals, solar glass, glazing, vegetation and upholstery. Start with reusable 1K–2K texture sets and use higher resolution only where the close-up view requires it. Check texture colour spaces, normal-map orientation and transparency.
Rebuild the warm morning lighting and restrained haze in Unreal. Test software Lumen first, with a simpler fixed-lighting alternative if necessary. Epic currently lists software Lumen on Apple Silicon, hardware-ray-traced Lumen/MegaLights as unsupported on Mac, and Nanite/Virtual Shadow Maps on M2+ as beta support. Nanite will not be a baseline dependency. [Epic rendering feature support](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
### Terrain and vegetation
Retain the existing terrain shape for the first release; introduce a native Unreal Landscape only if sculpting or foliage workflow materially benefits. Convert trees, groundcover and garden plants into reusable meshes with distance-based detail and culling. Preserve the open views and subtropical character. Use instanced static-mesh foliage; large amounts of individual Actor foliage would repeat the current scene's object-overhead problem. [Epic foliage workflow](https://dev.epicgames.com/documentation/en-us/unreal-engine/foliage-mode-in-unreal-engine)
### Tour, interaction and updates
Build one Blueprint-based local experience and a Sequencer tour. Transfer sampled camera transforms and lens values, then verify axes, cuts, interpolation and all chapter boundaries. FBX camera animation needs a dedicated Sequencer workflow or a sampled-data importer; it should not be assumed to arrive with the static scene. [Epic cinematic animation import](https://dev.epicgames.com/documentation/unreal-engine/import-and-export-cinematic-fbx-animations-in-unreal-engine)
Use separate groups for present buildings, future outlines, cutaway faces, landscape and services. Keep interaction logic separate from imported geometry so subsequent Blender reimports do not erase it. A full live synchronization system is not included.
## Work packages and effort
| Stage | Work and checkpoint | Hours |
|---|---|---:|
| 1. Compatibility and pilot | Confirm engine/toolchain; import one furnished room, glass, one tree and a short camera move; package and benchmark on this Mac. Choose rendering approach. | 4–8 |
| 2. Geometry conversion | Clean and export the whole site; organize assets, pivots and instances; repair openings and build simple collision. | 8–12 |
| 3. Materials | Bake/rebuild the material palette; validate indoor and outdoor appearance and texture memory. | 10–16 |
| 4. Landscape and lighting | Optimize vegetation, terrain, shadows, warm lighting and haze. | 10–16 |
| 5. Interaction and tour | Walking/flying, doors as needed, 23 chapters, simple menu, future and cutaway toggles. | 10–16 |
| 6. Performance and delivery | Profile packaged build, tune quality presets, test the entire tour and deliver editable project and local app. | 6–12 |
| **Total** | **Approximately 6–10 eight-hour working days** | **48–80** |
The pilot is included in the total. If it cannot meet a usable baseline on this Mac, resolve the lighting/foliage approach before converting the remaining assets. Installation/toolchain problems or substantial remodeling can move the estimate outside this range.
## Acceptance criteria
- Local Mac application launches without the Unreal Editor open; editable project reopens successfully.
- All listed current structures and contents are present at the correct scale, with no duplicate FAST/DETAIL export.
- All intended rooms and outdoor paths are accessible in walk mode; no falling through floors or invisible blocking presentation walls.
- All 23 chapters play in order, pause/resume/restart work, and each chapter shows its intended subject without unintended clipping.
- Future and cutaway toggles work; reopening the application returns to a sensible starting state.
- No missing textures or materially incorrect glass, black surfaces or severe light leaks on the agreed inspection route.
- Initial performance target: 30 FPS at 1920×1080 output in Balanced mode, using upscaling if needed. Aim for 60 FPS in Fast mode. Both are targets, not promises before the pilot.
- Benchmark the packaged application after shader warm-up over a repeatable exterior/interior route; record average and slow-frame timings, quality settings and memory behavior. Complete a ten-minute session without crashes or sustained memory pressure. Profile editor performance separately.
## Deliverables
- Editable Unreal project with organized assets and an asset/source map.
- Local Mac application, tested on this machine.
- Preserved Blender original and a documented export/reimport workflow.
- 23-chapter tour, room navigation and the agreed toggles.
- Performance results and a short illustrated controls guide.
- Optional recorded walkthrough video; final video resolution and quality can be set independently of real-time playback.
## Budget, dependencies and exclusions
Engine fees may be zero under Epic's individual/small-business terms. Commercial use must be checked against the applicable revenue and licence rules. Optional licensed plants/material assets: allow A$0–500 initially; no purchases are assumed. [Epic licensing](https://www.unrealengine.com/license)
For comparison only, hiring 48–80 hours at an assumed A$100–160/hour implies approximately A$4,800–12,800 in labour before GST where applicable. The hourly rates are budgeting assumptions, not researched vendor quotes. This is separate from the cost of constructing the real house and from any Codex subscription or usage charges.
Allow approximately 100–150 GB of working storage for the chosen engine, project, imports and caches, subject to installer options and measured cache growth. Avoid a source-engine build or large unneeded content libraries for this first release.
The installed Xcode is 26.6. Epic currently lists 26.1.1 as recommended for UE 5.8 and specifically excludes 26.4; this does not establish whether 26.6 works. Verify a compatible engine/toolchain pair in the pilot. Do not globally replace the user's developer-tool configuration as a default setup step.
Additional scope: Windows packaging/testing; public distribution signing/notarization; VR; browser streaming and hosting; multiplayer; a driveable Tesla; realistic weather/time-of-day controls; fully built future dwellings; a material configurator; photogrammetry/site survey alignment; substantial remodels; architectural compliance; energy, water or construction-cost simulation. The current terrain is a concept landscape, not a surveyed Samford property.
## Recommended next milestone
A 4–8-hour compatibility and visual pilot using the living pavilion, a tree, key materials and one camera shot. Its output is a runnable Mac sample, measured frame times, confirmed toolchain and a recommendation for the final visual settings. That makes the full conversion estimate concrete before committing to the remaining work.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
renders/garden-court.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
renders/reading-corner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
renders/training-area.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -0,0 +1,325 @@
{
"milestone": "2026-09-10-v004-garden-flow",
"files": {
".gitattributes": "d4f9cc15aed223cff7a906452370117fc93992272828cb1efbdd94c61422ad19",
".gitignore": "d38c3a1d58416906185c4c7695ea24f9401be3b610fcce0e624f41dff9fe1675",
"AGENTS.md": "233d2be130f8de20a4d17c9c5713462c97ed716be8682191db8dfcc646992999",
"README.md": "44086788e8fe8428b3e103744162b6a82625991902cf2b0ef159be1fb3c4298c",
"blender/bedroom-sleep-checks.json": "4f28426b4c23e55c956a4c5bc94947706da63e35de455cfaedd8e90e330f1114",
"blender/daily-life-checks.json": "22cf10e5f3acb11e92ed4780b08eb7c23bef1132b16a631e29cd895e81f068ec",
"blender/daily-life-layout.json": "a908d84f38a8ab035958b197c31037ea71e0e7efa5ed20da0bbf7867c5a85a1d",
"blender/daily-life-verification.json": "cc21f366de7c6133b7891c4fab23f075115fc96402aeaf27272280e9b7c5af55",
"blender/garden-flow-checks.json": "dde7aa8aef523f42963d2e069b242eda0e4f0bfb45b98f8924433b6b0c584876",
"blender/garden-flow-layout.json": "096b0674740c3ec2b739549f03213fd455b2158a9adf021b57990db372befa0c",
"blender/garden-flow-verification.json": "7f35c643afbc61152eb211a6c34a29b4edb98aefa261426ed0d5e99e9f296c70",
"blender/reading-corner-checks.json": "e6cf3b9b895888b4c20c95a9612070064067100d881da7e26b4d5f69bf4db50a",
"blender/samford-solarpunk-bedroom-tour.blend": "5c967f65d9e3f680bcf9e1da3f2d3d82bd4ab313fe030c0b0a284c2aca5d58b7",
"blender/samford-solarpunk-daily-life-tour.blend": "445db0cc7b4647dd45ae899632e3de5a2aaf1bfabdc2aa8ffc77d11c6a70ad74",
"blender/samford-solarpunk-garden-flow-tour.blend": "b2bf1e7556f75b3ad3d96dbf340feac0fecfaf0865fcbeb838b1c417bb62ae2b",
"blender/samford-solarpunk-reading-tour.blend": "f47296df39f3eaa381ec1b60615dec77f1ff5e2b5f3c645deea2e5b3af4e6d83",
"blender/scripts/add_daily_life.py": "701f246fe963124b04ef86512ff1465dd51bec335ca0059d6b966b653563be47",
"blender/scripts/create_site_plan.py": "7e7c566e960e37b5a8225157750ba11c06afe72d86fdceea7d73b2e6e0b03a24",
"blender/scripts/enhance_kitchen.py": "2ac50b24427210f00f6bd4ba492ddaffef22ee3835c20f3ec72d41b97081f1f3",
"blender/scripts/verify_daily_life.py": "7241568d786cd9d78fad97af24b14fd487cad1357bfac19fc1c800cbafeef8fb",
"blender/tour-chapters-daily-life.json": "d6eb5ff0406a5e3b54fd088ef3e050f876a0b922c2b94350770900bca3d6a587",
"blender/tour-chapters-garden-flow.json": "7a6807925630812188acad57afab5c46671b0d0c4fe6c8b90763f5f80353cea6",
"blender/tour-chapters-reading.json": "53d17d81ab55528240e6453c19fc7e45721c1ecfcf610e90e21644da509d42ab",
"decisions/2026-09-10-v001.md": "8c30365df61ef50a9bc06391e01eccc6e72c7bddd3d368defc34b720fcd90431",
"decisions/2026-09-10-v002-bedroom.md": "36d0a4a398b3fa8ca354e0f560c4cec4c96e788fa8789de41c11dad32d65c90e",
"decisions/2026-09-10-v003-daily-life-kitchen.md": "396ad405df9e7c10d1c71753ecc76cc9889c661e352533d601c666213f065c98",
"decisions/2026-09-10-v004-garden-flow.md": "55ed1e2e222941a9ab9dde5d0d18f6f979528d15e0e3fe988a340744f459b9bc",
"docs/PROJECT-CONTEXT.md": "0c03434255f9b7764cccb4551b792564295f8e476154a2172b905b999ef4bc4f",
"docs/REQUEST-HISTORY.md": "279c1f8b4c8f6a0c6b85754ffdd6aeb8b2d3980ba26ec852768d07dbe3e86e88",
"plans/architectural/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"plans/concept/garden-flow-plan.svg": "95c7309d44143ebb3f11a7e0f6b4cee8b076c17a423ab14c8288f450b54ae4db",
"plans/concept/retreat-garage-plan.svg": "225985399ed62625b0280ef9e6a802e276e3c6aad6c35e47c744628868c8022e",
"plans/engineering/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"references/UNREAL-PROJECT-SCOPE.md": "9727e4447a772756fae2456465f4677d63772858d3433e68c901adbd3a2a5632",
"renders/bedroom-sleep-setup.png": "5e12a5303615695093b61ef70111ee3d75c41ebe119c9efe0645f70725fbb707",
"renders/daily-life-overview.png": "fb7341ad4549c30b07696568616ae3304772307aea234d80214bfeeb79ffc834",
"renders/flowering-kitchen-garden.png": "22f9fa70bfa9a9c2f1f2e77efb97744d47dbfacbd9b0dd9b723a365cada974ec",
"renders/garden-court.png": "70c8bef1ae7b3c79be9fa914ca0e2edce7d097656957959fede9f7c1c48bc8de",
"renders/garden-flow-dining.png": "35fcd45bece74170974c7b1b7f001d6f1644041f9bab1b78b6b259d15bebcb8c",
"renders/garden-flow-overview.png": "145f950b8d41dfcb507b219637b0b2748039d6c743fe45c2b747f752c8e5d32a",
"renders/garden-flow-training.png": "0215b42b69ac098263e3edd1e434e60a28829a198caca48b2aa23566c800887d",
"renders/kitchen-copper-marble.png": "7264c703bc66d3ab574224e03854399972301cd70ab1cd161d92eba1140a5563",
"renders/meditation-room-unreal.png": "8fd03a477c8aba27fad0d36c8eb6fb58cc0c1926919d618985dc4b8f553f3412",
"renders/outdoor-gathering.png": "94f1d80b20e4a6f9026493f7244b83c8f28a9b39f5c5c2043ddb94a3216324bf",
"renders/reading-corner.png": "eebb89ce1081bbe458d95c22888346ee6e8175e2fb7df88a0694f0b90601564d",
"renders/retreat-garage-overview.png": "83beac9982533420f31af41048f6e08c7ff4602d0ea4086b6d77649de934a171",
"renders/tesla-solarpunk-garage.png": "5c779a173a748edd5b10ce18fa5e90ea60226614dd4b06110c38868b68616deb",
"renders/training-area.png": "4534ad28d7ccda82bc93f44f5c0b8b358a3f9e549fe4ba141ca94931812678f8",
"site/lidar/processed/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"site/lidar/raw/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"site/photos/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"site/surveys/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"source/samford-modular/README.md": "e4dd2ed10b0e4f33ddfae86e753cc572d7872bce67ba9aecf66557befc05324d",
"source/samford-modular/build_scene.py": "0bce8490a7f078880c0902b464e9f0202e30f2b44673a7b5378d248850264aa9",
"source/samford-modular/check_framing.py": "1995123ec16be90a54c3de8d6c220b14b233e984e9a7ddb0f050e559fe6eb170",
"source/samford-modular/create_layout.py": "7ad200bdb939a5752d84b0517138479cf49ef8813e116b2a81c163f2ce6d5de4",
"source/samford-modular/finish_scene.py": "042f5400d74139e23e6346bdf42dc78ccb120ae684d07191686e2592b83b68ce",
"source/samford-modular/refine_scene.py": "6fd06b2e2cb9e67f783ec0a50884cad263a634f2301afbac17ba4fd4ee96ad35",
"source/samford-modular/render_detail.py": "7583f11bff0915e508db346d2e2e7d00ab40a65388d8df9f353eaa35420922c2",
"source/samford-modular/render_final.py": "33a016fc70f365ae7a52854cdb172c77fcbb9aec7699080ec06def604a337927",
"source/samford-modular/scene-checks.json": "ba8cc36bc1fd6087ca6c3e689449b0aa75d5a3fe07ebcc9e8e762d4153989a0b",
"source/samford-retreat-garage/README.md": "ea463e21ea71158e75a17eea6ee0c4e666663ef18dd58ae74d64c2b09f201a0f",
"source/samford-retreat-garage/UNREAL-PROJECT-SCOPE.md": "9727e4447a772756fae2456465f4677d63772858d3433e68c901adbd3a2a5632",
"source/samford-retreat-garage/add_daily_life.py": "701f246fe963124b04ef86512ff1465dd51bec335ca0059d6b966b653563be47",
"source/samford-retreat-garage/add_reading_corner.py": "95a5223670ae53343f6fe7e435c0955dcb0c7d05dea3804b2b3cca356c02feb9",
"source/samford-retreat-garage/bedroom-sleep-checks.json": "4f28426b4c23e55c956a4c5bc94947706da63e35de455cfaedd8e90e330f1114",
"source/samford-retreat-garage/bedroom_sleep_setup.py": "584ea23c5fc9e7b1651025776d25c9ba18fe4b8482a539f9e84cde397cdc2e08",
"source/samford-retreat-garage/create_garden_plan.py": "1bc15d6e847af799755ae6dcc857f096d8194c0f47db79527a7eade53a4d725c",
"source/samford-retreat-garage/create_site_plan.py": "7e7c566e960e37b5a8225157750ba11c06afe72d86fdceea7d73b2e6e0b03a24",
"source/samford-retreat-garage/daily-life-checks.json": "22cf10e5f3acb11e92ed4780b08eb7c23bef1132b16a631e29cd895e81f068ec",
"source/samford-retreat-garage/daily-life-layout.json": "a908d84f38a8ab035958b197c31037ea71e0e7efa5ed20da0bbf7867c5a85a1d",
"source/samford-retreat-garage/daily-life-verification.json": "cc21f366de7c6133b7891c4fab23f075115fc96402aeaf27272280e9b7c5af55",
"source/samford-retreat-garage/design-checks.json": "a9fb273c1bf65e58b42c8645b3be2dc4403f4f17e1949207b1620fb207a34bbd",
"source/samford-retreat-garage/enhance_kitchen.py": "2ac50b24427210f00f6bd4ba492ddaffef22ee3835c20f3ec72d41b97081f1f3",
"source/samford-retreat-garage/export_meditation_unreal.py": "e17577438c2186345615726d170a269384f2921314bcac0f997ee6cd5345a169",
"source/samford-retreat-garage/finish_and_render.py": "26e53564bbdc013ec9a7466ade32d8fb4b20ed98704a00d43ef88360ac59a99d",
"source/samford-retreat-garage/garden-flow-checks.json": "dde7aa8aef523f42963d2e069b242eda0e4f0bfb45b98f8924433b6b0c584876",
"source/samford-retreat-garage/garden-flow-layout.json": "096b0674740c3ec2b739549f03213fd455b2158a9adf021b57990db372befa0c",
"source/samford-retreat-garage/garden-flow-verification.json": "7f35c643afbc61152eb211a6c34a29b4edb98aefa261426ed0d5e99e9f296c70",
"source/samford-retreat-garage/meditation_finish.py": "8baee8e7c47bbae3202e87626e0ea05288f9465c59f6160c7ad887a8ee58ce40",
"source/samford-retreat-garage/optimize_playback.py": "bccd4ebf182fd656e7ac09af4ec9101d3e7b8a5fae21a61dfb9074455660cd79",
"source/samford-retreat-garage/performance-checks.json": "87a1b041838216b7b1748b24cf7f3412928c2561265f68f63b9655caeb196be2",
"source/samford-retreat-garage/reading-corner-checks.json": "e6cf3b9b895888b4c20c95a9612070064067100d881da7e26b4d5f69bf4db50a",
"source/samford-retreat-garage/refine_garden_flow.py": "15844609e47444024f7148734c155794efb0ad76f31ba37c03acc635750bca67",
"source/samford-retreat-garage/revise_scene.py": "936b86887f67b465d3803f7df3d54579cf4f867a7d377aafba61bcda373b5017",
"source/samford-retreat-garage/solarpunk_tesla.py": "5d16e733a8f7f06b306cd6fb1962e66a98e5e1b619e4fd67360e1f1e965e7f2b",
"source/samford-retreat-garage/tour-chapters-daily-life.json": "d6eb5ff0406a5e3b54fd088ef3e050f876a0b922c2b94350770900bca3d6a587",
"source/samford-retreat-garage/tour-chapters-garden-flow.json": "7a6807925630812188acad57afab5c46671b0d0c4fe6c8b90763f5f80353cea6",
"source/samford-retreat-garage/tour-chapters-reading.json": "53d17d81ab55528240e6453c19fc7e45721c1ecfcf610e90e21644da509d42ab",
"source/samford-retreat-garage/tour-chapters.json": "7ba0647e7c9102e1f9cf87ed35979b1a110f335ab0439bfbac9280d1d826de49",
"source/samford-retreat-garage/verify_daily_life.py": "7241568d786cd9d78fad97af24b14fd487cad1357bfac19fc1c800cbafeef8fb",
"source/samford-retreat-garage/verify_garden_flow.py": "b48c25116eb7f3cd56914a4f53601427207c387dd59d48fbd3b8cb692a9e46c0",
"source/samford-spacious-tour/README.md": "346f05b5f2aafc8b9f846f55da19ad33ae6a3ef917dc6f95902c78954791319d",
"source/samford-spacious-tour/build_tour.py": "54bd32d6c27f8c594695e5463672eee299fd70ec08a66e02b77a1e5b571b6b50",
"source/samford-spacious-tour/children_and_final_tour.py": "8934aebd991b3cf8318d01afe7662f6713f27cdf3f4ac78c30612f33fc0bf6a3",
"source/samford-spacious-tour/create_family_plan.py": "a2b9fc3c7e246591936968e1925ea5f7c71bc5816005f0f5d4c037679b576f26",
"source/samford-spacious-tour/finalize_and_render.py": "85d21a9bfd0f50a84882b947f2cf2ec4f93aad19748c82d70b06baf3645da9db",
"source/samford-spacious-tour/inspect_scene.py": "2cbe2ae93c9df1c6a8e6a678f6487f9a1e832f3899dba8a836e6acb6b191bcc8",
"source/samford-spacious-tour/refine_adjacent_bedroom.py": "1fe6995142d78d0de38be4c5a50feca02b77ce396de0553055d61dc0f68acb11",
"source/samford-spacious-tour/save_ready_to_play.py": "b25bddc10469a2a050f7bfacab4be2d088892386062f4e58ab11cac8806a9887",
"source/samford-spacious-tour/tour-chapters.json": "e5472d288a42e1c32618ffa40e27eeb418675f23ea7391e02164f2022bf5e213",
"source/samford-spacious-tour/tour-checks.json": "84d18a75b9e5ad2ded578deda0be163f37a32e058dfec62377599f3408ae3287",
"unreal/MeditationRoom.fbx": "7540b71e6cf075e51748db7dc829cf7330c1bfa8cfd8c713041f2127f17ceceb",
"unreal/MeditationRoom.glb": "529770faf5d746f74cc33ccaa84bc1315b1dced2a2e6d14209add716983a9fb6",
"unreal/SamfordMeditation/Config/DefaultEngine.ini": "012fbb2dee20d5ba6c074503d52b77d76d013c241f791ac3b09065336888ac66",
"unreal/SamfordMeditation/Config/DefaultInput.ini": "e1b7a60e4da1acca28b8540ed7b2746551df9ae1ddc0056eb45a0932ed82ea78",
"unreal/SamfordMeditation/Content/Collections/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"unreal/SamfordMeditation/Content/Developers/marcus/Collections/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_00_Unpainted_hardwood___long_X_grain.uasset": "9a066173dbe5db1c0cb0afe5248e4e1261681bda11b6527d7f3af125f9e79c72",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_01_Split_sandstone_facing___mineral_grain.uasset": "d799fb9c6431e985d944fbf1ec01154af70be2233fb3dfec394352d7cbae3d82",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_02_Reusable_dark_steel_bolts_and_shoes.uasset": "9816d93a77f448535e8276f79d7a16b8184d7594d05f219e6a102416ce0e099e",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_03_Soft_warm_clay_plaster___natural_mineral_texture.uasset": "3b14e48ac6cd5a307bb449ab6c885d25f82a406477064832d97fa6de7640d747",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_04_Oiled_spotted_gum_posts___vertical_grain.uasset": "6e994d20055d07bcc300602967ad9b3573ad5140387163718ade802292d10e06",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_05_Clear_glazing.uasset": "ab6c85cd887eb365b04299513612bbd1ab1932b25766185932cf556689a5c056",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_06_Naturally_silvered_hardwood___horizontal_X.uasset": "037ba2e53b28c54b52ebb9c6c7b32d753576161e3cafa204cf751049f4882911",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_07_Bamboo_lining___golden_fibres.uasset": "ea8c73a6a9ddd85d547f993fbb09896ab8a88b64f86feb9437c21cb5b4fc0944",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_08_Pale_Colorbond___powder_coated_steel.uasset": "1533bd91d752f71fccc11dd73da1d9f47119392e689413f6a3b599d6924b004e",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_09_Woven_natural_meditation_mat.uasset": "0cc2724e4c9d20cb71da1450f2d550b1184f15cf160b6b214525461cc0d2ad66",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_10_Undyed_flax_linen.uasset": "e07f04b7d82026223e8d4f418abb1f00fda6039aca7ce09c8d47ad790b9b7e5a",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_11_Meditation_cushion_charcoal_linen.uasset": "1f841bcd9a0db477c4880c1f2b05e4486f37a43d1799891a9e8fc8977eba654b",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_12_Beeswax_candle.uasset": "d00416915f3dc708aeb0a3dbd6efcf7de4715deb3eeeb87be91a413efe930987",
"unreal/SamfordMeditation/Content/Meditation/Materials/M_13_Small_warm_candle_flame.uasset": "453fe28e64351165760781b00ef86da99cbebf54699ac59cb45808e6dad3ef6d",
"unreal/SamfordMeditation/Content/Meditation/MeditationRoom.umap": "b62d947b18d79ee64d08b99f3bbacb9bb67439778c8331899e3d32445ea4b3a7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_000_Meditation___timber_insulated_floor.uasset": "12fdcb34db09a0ef263cf2da5fd446684b9a45a707a018019503773dfa4519ad",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_001_Meditation_room_stone_pier.uasset": "97e9a418cdced4fd7b6daf323e915550a9077a6d5e441fd1995908bea6910a0a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_002_Meditation_room_steel_post_shoe.uasset": "c4cdc5d329e44acb04a8ec7ef972db30fe774a8c3ecc3810eefbd70feeecbd68",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_003_Meditation_room_stone_pier_001.uasset": "3e6620244701174b705324502b0963e36d7cb29d961cf92abc1635a612888efc",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_004_Meditation_room_steel_post_shoe_001.uasset": "9e3f33455f6482815a291e564484e948153a96f620ff71427c27a9cdaba591d4",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_005_Meditation_room_stone_pier_002.uasset": "544b130cb3f7b2d1d6cbffbad34eae78d5a8d1af185fe88d61ea847bc2a2b226",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_006_Meditation_room_steel_post_shoe_002.uasset": "e600ed2c7e8c5729a4bef78b4b365a5d1b80737a0716d4ab2c8981b38aaed635",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_007_Meditation_room_stone_pier_003.uasset": "06c808d337af1eb3d20a4112d399d4788ff63423e57be1a6e4705bffb5b84a2c",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_008_Meditation_room_steel_post_shoe_003.uasset": "e0886fe68c23c8e300307b03da42a12c7583b29ff7af113c276eeecc1c6c49d3",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_009_Meditation_room_south_clay_wall.uasset": "db3403c6caa0661f82eb0a0bfeca91e320f79df495d1b58f8f5512a820e2ea77",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_010_Meditation_room_south_clay_wall_001.uasset": "78dbcb69970099349fd8e2cc27e0ce75b7cc01229f5041baff725aa0c57b8427",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_011_Meditation_room_south_door_header.uasset": "b4b443081c2c7d5148fbbe2ab888156bb6741abc2bd175de02ace8659f8bb41f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_012_Meditation___independent_front_door.uasset": "1d888b6f2f1039d8b938c63ee5d66e92a19860b8ac129c69b61395cf65bc11f7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_013_Meditation_room_entry_timber_jamb.uasset": "0f81974437368daa8087f4761f43e7ecb00d0c07a0bdfa1215d4d4ec47bf15d8",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_014_Meditation_room_entry_timber_jamb_001.uasset": "e86a963fa77b6f3419a1b913ed610ed84dc526d6ab91fb19930aaacddfe101c6",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_015_Meditation_room_door_handle.uasset": "de616ce8dc6885bf5c962b6bd58799c7b8fa83f76b73027d8c8683833e5802b0",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_016_Meditation_room_north_window_frame.uasset": "7bc1dbfd0fc97a1c9931c1a656eee6371b4a38f34ed1d502a63318c26de8acff",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_017_Meditation_room_north_window_frame_001.uasset": "bb7dec4ae82b43c725f722d64744d02ac0716156ef7e043667230ede09b8b5a3",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_018_Meditation_room_north_window_rail.uasset": "a10db8336666600077870267f22a26abb8a8cf8973659d7dbc60880d3b552564",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_019_Meditation_room_north_window_rail_001.uasset": "471a8d651b406bf9fd1cf22c190c989897f2e23e2a9bde64a350e8c502ef0ff7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_020_Meditation_room_west_clay_wall.uasset": "eba72889a31bc090b140c8583b756eee5efe9f4cb200f71c81260b1509aa1efc",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_021_Meditation_room_east_clay_wall.uasset": "501c93b65bacbb00a9e2e64c410b108f01a195780c228c8eba6590627364f58d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_022_Meditation_room_east_clay_wall_001.uasset": "97472f2df63554ac6080770bd5351e3f4eac7d964150210accec8638d694d1c5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_023_Meditation_room_east_window_infill.uasset": "59ff953dc722ca778de4b9befa3a45aa7e1e65f65f05d7c0cbd499ad0b4c2e0e",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_024_Meditation_room_east_window_infill_001.uasset": "4a895548e3b6d09550f259a642a3216a9bb8b7c02b3aeaafaa6569d5fca4e9b5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_025_Meditation_room_east_openable_glazing.uasset": "535772435334867c655aecd71603ff247af3c69f28e95789ddc6aed5edc8fb8f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_026_Meditation_room_front_weatherboard.uasset": "2aac25b98e3c880807191c1f48f9a8fc9cade1d53fe9dbdf7bd56f8ec7857762",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_027_Meditation_room_front_weatherboard_001.uasset": "1dea5d6ab2f6cdfbf6d7c19b7568dc16c1af0b838d760a0f90961fe33c98397a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_028_Meditation_room_west_weatherboard.uasset": "a3be4c32fc92ee6fad82c7cf95dd65794751e8f9f8db3232e40149efd7e1d94e",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_029_Meditation_room_front_weatherboard_002.uasset": "2156b587076e72e325ebf5effeef1510d6d5c265cf06156a2efbf3fe765875e9",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_030_Meditation_room_front_weatherboard_003.uasset": "bf151a8eb87c12cb9a602f98f3ab71f922a3f24f0e9390798d299387dd0c4e25",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_031_Meditation_room_west_weatherboard_001.uasset": "dec2e70cf14cbf484bfa85938bc956033287dfbf1554093dd47a6758ce855667",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_032_Meditation_room_front_weatherboard_004.uasset": "9da124687d8a71f9699dbd004a2e388e8e0b19118d1ee8b2173db5ce3659c52f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_033_Meditation_room_front_weatherboard_005.uasset": "40ad7d87fff8a79bc34058864f4b1bf316b3902695974f9bb70e572a7676ee8c",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_034_Meditation_room_west_weatherboard_002.uasset": "77c9cc1b37842f4cf52db5162a6da6980a06202c9d973b4ceb1c26313c70e7bd",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_035_Meditation_room_front_weatherboard_006.uasset": "0f58f1cd5dc0c224490617a6a1981760c5942cdbb6cf92bd8738053d2faf1eb8",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_036_Meditation_room_front_weatherboard_007.uasset": "0187abcfb01ceeb197cff1842e649a0e68b210603bf90777de5aad8ed20eba9f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_037_Meditation_room_west_weatherboard_003.uasset": "17505ea4ba0398b75558e67278e035587f4a31134f2737e738a751b8a688f009",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_038_Meditation_room_front_weatherboard_008.uasset": "9f1ef068c616a2f5ce31c342c751995b14be8a9a66dd36eaded2541070741382",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_039_Meditation_room_front_weatherboard_009.uasset": "34e01852a6f0f374b6c7beffe413656fcba29a33979c211043b263a3fe613519",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_040_Meditation_room_west_weatherboard_004.uasset": "3321ee44e47c077a35d7ea530c7077c26ecee7241b84c7ed3d715309cca907ca",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_041_Meditation_room_front_weatherboard_010.uasset": "4a7b7dfa820d2ffe1b612fa112008978f80475f90c9c40a1f4ea40945cda5950",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_042_Meditation_room_front_weatherboard_011.uasset": "0d9fe17c06592b8039e0c77cc5a2e95dbc9f65e5de655f831166caed209c52ce",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_043_Meditation_room_west_weatherboard_005.uasset": "10dfa2cc21fec69851fdff3b7ab15e8590e7350f7f5d5730487c8b44823aa3c2",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_044_Meditation_room_front_weatherboard_012.uasset": "ac6351033ff2c525d8fc5b0152f16a89e7fbc126e57e7dd11326108a3d0a1f7a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_045_Meditation_room_front_weatherboard_013.uasset": "ed3206761163fff55fe00d6a925e731512fb17b0fc4b7588e26fce0ae12e9c53",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_046_Meditation_room_west_weatherboard_006.uasset": "05c3398168d318506fdd0c9255c0b60ef082bc41c223cfb6f531c4c8940018d3",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_047_Meditation_room_front_weatherboard_014.uasset": "ac6b03616a1459998fab75fed56e431335502c4f08560a42af94a97a06b62f40",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_048_Meditation_room_front_weatherboard_015.uasset": "7f8269ca00e93f7f92d3553a661b5587ba7dc7db91f1a5d02b3edbe360c17398",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_049_Meditation_room_west_weatherboard_007.uasset": "33662967db3e81cf6430c99e2e6fd43a16fbb9d639be4e8204fbfc9be47e21fe",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_050_Meditation_room_front_weatherboard_016.uasset": "79eb980b9a227616780e93a54f8db829d9abd78a2c59561b474da1e2c41aa5ee",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_051_Meditation_room_front_weatherboard_017.uasset": "a2f7eb2453df6e42f1b475383c4cc9907bac432f340de8019bc5089ac3b0f405",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_052_Meditation_room_west_weatherboard_008.uasset": "c0ab0f823c4024be18a3ff415dc2f151ac8c8ac6a6eaf46e72113315e4590c59",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_053_Meditation_room_front_weatherboard_018.uasset": "5ec79aacae642e8e12f3236bf74c2eb43fd3be2aac9a51df64a0c72e712ab325",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_054_Meditation_room_front_weatherboard_019.uasset": "5ba23b4315a339bb84667f8777721bb97c50fd8cc70ee2c310c96c9f8f760bf1",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_055_Meditation_room_west_weatherboard_009.uasset": "6ea41bfe5a8803f9b492ba20c56374ddb80b4197d08913c64e8508c05f785412",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_056_Meditation_room_front_weatherboard_020.uasset": "e6fdd8c25682258cc6ae778f2ac93fbd7476bc91efb3948f09440b940e39a84d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_057_Meditation_room_front_weatherboard_021.uasset": "6c10ead07afdcb9a0d139744f36062dbba3ebd4948f72d48858b98f33098050c",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_058_Meditation_room_west_weatherboard_010.uasset": "067ea03d4c96834c9d144b982d8741bb0fe472360785149e39abe7cf7035c5a0",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_059_Meditation_room_front_weatherboard_022.uasset": "fe5af3c78967b8121cdaa5bd01444a1bc4aa6759da4001e166a89704d9b18993",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_060_Meditation_room_front_weatherboard_023.uasset": "159353e65382dc6b89c5ad8aed936d7927769d010f10fb615433e0b6130590b4",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_061_Meditation_room_west_weatherboard_011.uasset": "4031a830f675be92bf953afeee58c35377c12771524b5ca8794995c1154cc8fe",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_062_Meditation_room_front_weatherboard_024.uasset": "f458b318e258607f4f2edca29846728308f3ce56d5808cbf5153ed015c5be1e7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_063_Meditation_room_front_weatherboard_025.uasset": "da04c5cab14858e51dfe832ff56fc5ad4c095c056dd861c20634544482760ad4",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_064_Meditation_room_west_weatherboard_012.uasset": "a47a6a20278ebd06c6e9fd9ee5f7233d8e2540ef19ee7d591f24662e17d5f771",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_065_Meditation_room_front_weatherboard_026.uasset": "987cbc5338f58e2e54c0f1a8ce8f1551b145ce9780b40ad017afd90f15254c64",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_066_Meditation_room_front_weatherboard_027.uasset": "27f28c28a5c095b53ff651ff7304808f43b4fab1d2a79ab5e8123a94fa4b86b4",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_067_Meditation_room_west_weatherboard_013.uasset": "025cf8a1f83ed5b091a3acd1fdc57a950cd049fa8aadba3446a465b1dee884f5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_068_Meditation_room_front_weatherboard_028.uasset": "16d75865a9b8f7306df9cc4235dbbc01f8b5025169796ebc334678cb731e27cf",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_069_Meditation_room_front_weatherboard_029.uasset": "fe3b4ab0a3ca58933125da1e194c79bd200b6bbadf7d56461b8b8ed276a993a5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_070_Meditation_room_west_weatherboard_014.uasset": "3c47686ddd5f8795382d262ba8dfda5fec80e33e46abab97bec3a4ef12960e5d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_071_Meditation_room_front_weatherboard_030.uasset": "facb386c13f2a75ba0e8b578f00584f1bf7d97ad508448140aee6e0b97ff1d9f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_072_Meditation_room_front_weatherboard_031.uasset": "f91ea64d84ac3ace21289ebd38952996fca0007067438ede140176070b028d20",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_073_Meditation_room_west_weatherboard_015.uasset": "68b2cac9bb857afde98c961029a1fe08a8f095f3aaed820d86fa7a7525f72c69",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_074_Meditation_room_front_weatherboard_032.uasset": "b21f77f8bbf0154c82c177394ff9ea6892cea4d7a01f37485fe2d453b6e848f8",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_075_Meditation_room_front_weatherboard_033.uasset": "e91e2ef0d9fb04844da4cd3dc1f2a099de10e21ec6198188d9ac5ed6e8ede299",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_076_Meditation_room_west_weatherboard_016.uasset": "9dfb6ddd162418c9c0b438c29bcfd11fefaff7eb85badbdff7cc2daaa395f54a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_077_Meditation_room_front_weatherboard_034.uasset": "40c2dab0e272f5932ec807c200cb597b4438d8e9c71dfa977ca5150c653610ea",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_078_Meditation_room_front_weatherboard_035.uasset": "18998b1a035633e6e387c7ad5ed3cf1c3aeead7f78d9d696263228de61722a5e",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_079_Meditation_room_west_weatherboard_017.uasset": "dd8b72c4fe7aaca0af1bdf4dde6d927ed376f369d1c53335179a4a30fa80a110",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_080_Meditation_room_exposed_corner_post.uasset": "5c266256c2655e4f0b36b56ae6e3daffc3625853c2db1b61406c642a6c0afd65",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_081_Meditation_room_exposed_corner_post_001.uasset": "d80f0f5b585a2e5fa043a34ea0d325449d9b9dcaa3aa2e5a20a7d6f9bb9cec98",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_082_Meditation_room_exposed_corner_post_002.uasset": "2225af21f5572a9daf2b09a20e0eb4aa641e83c93cae0e7c62c71880270a4dd2",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_083_Meditation_room_exposed_corner_post_003.uasset": "f437cf7736b5617b744ea9eaad9f2319fab110c4978cab71fa748e65d5ee2ef6",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_084_Meditation_room_exposed_roof_rafter.uasset": "11947084518e30c5a1f6d6f125b21efb40bd88cb1b0898d384ff856c9de19c80",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_085_Meditation_room_exposed_roof_rafter_001.uasset": "5436b764cb44fa86e4c99dc099cf5c70edd63556e9d7fd48429ba5e03b009721",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_086_Meditation_room_exposed_roof_rafter_002.uasset": "37cade73b65228bbbb123704d7b2414a1a92171c80b4cdaadacb504abc82fb88",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_087_Meditation_room_exposed_roof_rafter_003.uasset": "bf66c840bbf6d3709564532c80a4f5130e290d9d0e5a59e73e096a1c3161d76a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_088_Meditation_room_exposed_roof_rafter_004.uasset": "d0354139f2ccbbeca430d86db210b856efbd7ebc6173efe0d74f40e093927576",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_089_Meditation_room_exposed_roof_rafter_005.uasset": "c90256b6dbcf659632dc7e22f400759991a1d6d24294a329e570419fb60f4aff",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_090_Meditation_room_exposed_roof_rafter_006.uasset": "ce8e504ee2483923c368bdea7f8feb04f80b45281f687823aea2cd57de845757",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_091_Meditation_room_exposed_roof_rafter_007.uasset": "ac16a7ae6b2cb658507c0d9fa0dcff06c837eff541b96bd6e5eb44c4ead42685",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_092_Meditation_room_bamboo_ceiling.uasset": "dae15129671962fbd2409f56a537b51d992cb38fe41ff044890a4f15add52f96",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_093_Meditation_room_bamboo_ceiling_001.uasset": "a6a9e0a40f05aa0d4422b6523d9c27b61483c8d82a4947d2fd9c4331aa8722e7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_094_Meditation_room_bamboo_ceiling_002.uasset": "4f08f84800ae55a07caeaab8fb7ff378261746decc67b018ca7a12de87defbc0",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_095_Meditation_room_bamboo_ceiling_003.uasset": "60f87fb89bd5fc0ce2c239fa48a6a1bae35d40a0e05bf22af00ecb1f3c605343",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_096_Meditation_room_bamboo_ceiling_004.uasset": "db238f7c0404e00db1aeff4635d5acb0fa122a06678bb24a51cd08f177f01279",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_097_Meditation_room_bamboo_ceiling_005.uasset": "c3358a7794167a8089792c6b02413b7f202bb89dc87433157ca5ed0f54445495",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_098_Meditation_room_bamboo_ceiling_006.uasset": "3741409a4d15d89c981ed5a542dfe00680ac7d10ccaeb8f50471fedf8ef0a41b",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_099_Meditation_room_bamboo_ceiling_007.uasset": "560485ba0c6c0e3718c553c795467f0a4e272f10c5cb2b6ec840d7d23dbafcf5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_100_Meditation_room_bamboo_ceiling_008.uasset": "17fe1d033a2ff4625f0621cee02d37380a3dbd2006060fcedf4e05e84c972272",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_101_Meditation_room_bamboo_ceiling_009.uasset": "74ca194754c663f6bf4944e8ade4cfc78ac043f59ac090f220383385d17c5ae6",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_102_Meditation_room_bamboo_ceiling_010.uasset": "b35f1ca01f34c52a1d9c88be8587c463f9c28ee4b940c24bd86d6414c365aa53",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_103_Meditation_room_bamboo_ceiling_011.uasset": "c0d3f00d228bc0833c6c8e85eebfab8b8e04cc1d759b1d5326dc57041fbc37f8",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_104_Meditation_room_bamboo_ceiling_012.uasset": "cd40c69779401706e2ad4c09c339699fb13ac54d58037a5722662dae16f30dd7",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_105_Meditation_room_bamboo_ceiling_013.uasset": "a2d1f1adeda904c7cd052a0761fa2780cafdd7e1da57d30aaeb5d84aa76b0e07",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_106_Meditation_room_bamboo_ceiling_014.uasset": "6fd5248411b907064016a11219ad4d2228193acb5a33aed6b939190fbe16362e",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_107_Meditation_room_bamboo_ceiling_015.uasset": "9bae966ff397ac6cad4d4e80240ac3b297f4e9b15b5399bb547c50d694e2d5ba",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_108_Meditation_room_bamboo_ceiling_016.uasset": "db60293cf97cb8280e8162a8d5ca073238668a61c296e4d85b839e0e35b805c0",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_109_Meditation_room_bamboo_ceiling_017.uasset": "55c18d691366784003d420a22e400ff2f419902cba45af2b3d9a8881e41b9a20",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_110_Meditation_room_bamboo_ceiling_018.uasset": "d1dc6438679b871b9f1530db92121014918a50e4346493a402acd4f0ced6e21f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_111_Meditation_room_bamboo_ceiling_019.uasset": "6f3a13c442142dfe25bbecb712fda1002fe720d608ab73cef437ce4bd1fdd572",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_112_Meditation_room_bamboo_ceiling_020.uasset": "f70d43c1a66ca177e982ea74aaccfbba73fc5111b811863739894682b166986a",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_113_Meditation_room_bamboo_ceiling_021.uasset": "12895b1042619edd0e2bb3063eb5dc1cf4640bddda267b8c616e62e119e55cf3",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_114_Meditation_room_bamboo_ceiling_022.uasset": "3c56880ddf9b55660abc366da2230e8ab2e7083cb81e113d9a5841d7250e8257",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_115_Meditation_room_bamboo_ceiling_023.uasset": "2fa2136359c3b4601284d7b634d9c2ca3bb6d41de5d5799ed737f65aea7449d8",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_116_Meditation_room_bamboo_ceiling_024.uasset": "d6d16e506af79f0c6e98fa9972f9cd97fbdbd5b3867836d0bfd79ea4f6feccd5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_117_Meditation_room_bamboo_ceiling_025.uasset": "c2238ac290214b08ad9c8069db9ae65dc234f967edd6c2c16a1cdf78a94b4fbd",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_118_Meditation_room_bamboo_ceiling_026.uasset": "65cf5ffaf8f3dbad00c62c2d8c9e62828f518fadc7134d6d21c9ed6ad3f968c4",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_119_Meditation_room_bamboo_ceiling_027.uasset": "15ea884a30192440f6830693c230d3f2698f4771a73595f72d000a0a94602950",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_120_Meditation_room_bamboo_ceiling_028.uasset": "23d80d76fce35d19431334cb360fd8a935a101d6bf35052385f21114479dedcd",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_121_Meditation_room_bamboo_ceiling_029.uasset": "dfc0f5bd104e5acdd3d40a9939eee02277ec00e29a522ec235a17609ef1694a3",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_122_Meditation_room_bamboo_ceiling_030.uasset": "e01cc5da158b7c18a4e606b59a141582997c489ee7299fa4a80a281fad8b012d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_123_Meditation_room_bamboo_ceiling_031.uasset": "7cfb9ab72e52479a2682676e27c62ea24bf8ed90599db4e15b66963b1e9ae92b",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_124_Meditation_room_bamboo_ceiling_032.uasset": "4a2cfcc535e30083f49f37b94bebe268adb1e52cbd3d1303fc928c55490ad6a9",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_125_Meditation_room_simple_mono_pitch_roof.uasset": "27305e4a3e2ff3642cc2990939a0529b067e47612c6b2e1b3c387f82713c21ab",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_126_Meditation_room_covered_entry_deck.uasset": "bb78c8eac8c56703cd55886db22269375de8ee77fccafc62105789032505340c",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_127_Meditation_room_entry_awning.uasset": "412a0a5e26e87c76e5544e4a02f9221eb9308cf6488247436e02c0f8fb642a16",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_128_Simple_woven_tatami_mat.uasset": "34eb9f074904ed7758882fce9e55ed1d417fee2184100daf43ad9c6722c61980",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_129_Meditation_square_zabuton.uasset": "6e9801b9999dbdc721613f9f39511174b075f80502e92441a0311d00f4ac3e19",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_130_ROUND_MEDITATION_CUSHION.uasset": "164f071fcf54a54abf390fc82b7bd252848c7e0539026e4107fb8cf5b686ab3f",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_131_Low_stone_candle_shelf.uasset": "8fd41dcfc3a4e2f1094c92ceca07358799f8067e94efc2ebe1eb4b608f85aaa0",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_132_Stone_candle_holder.uasset": "f45337fe1282fb5aa5b5ab80a103688c2ba245b22c37ad0ac3cd378614308591",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_133_Meditation_candle.uasset": "005632e9b3b023f00c791ed31de46b4fb47430afc520faad70bab3cb52c5f384",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_134_Meditation_candle_wick.uasset": "8477be54d47d6f0c1207d9c2709c56c5255af163bc6571fff7e853398fce5560",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_135_Small_candle_flame.uasset": "2627b015f4cd413c549b5fd647104458a1c6a69734aece48b8c89125601e9602",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_136_Stone_candle_holder_001.uasset": "fc78a68501d5c779e43da01c247ff51c2edc17d2d7b968d8431a6979e697ccfb",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_137_Meditation_candle_001.uasset": "03036f954bdc915a442c34576ce5f2a8e00dce6cb4aa26ea7c688084513793bd",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_138_Meditation_candle_wick_001.uasset": "eaac00b782f0d57c3744a6dd093c6b44456cf285ffec397a1860eacca52c6f21",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_139_Small_candle_flame_001.uasset": "e19ad2080f43f2f13d3e02c1d2cc60d4831e909943ab570d45ee63a510fa3c10",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_140_Stone_candle_holder_002.uasset": "d7626289ae68252337e209caf30a72db9ed1a0c7448b39be9c712ee02882788d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_141_Meditation_candle_002.uasset": "1f4729d2018a39ebda9241d437b7f4f2f79dfbe3e1202bd4c72b53168cc4c44e",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_142_Meditation_candle_wick_002.uasset": "6dfa36b24fb19e953aa232fd5f41ec20b2b1821b432a870c8f45bbe797cfc72c",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_143_Small_candle_flame_002.uasset": "048f7bdf8579eb8054e3eaeb6add1ee575785df21f3b18889329caf0677967da",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_144_Meditation_low_timber_bench.uasset": "ae49a0cce4c2d79c119f1eb7829949dfd755f5695111c36e7ec1bed3d967e339",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_145_Meditation_bench_leg.uasset": "bfe19dc8d0dd67bec918972cd2c0d614504b669507c193bfa77055f59809367b",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_146_Meditation_bench_leg_001.uasset": "5ecf154402d20d03516e59b9ba615c6153822377f227dac9895ea77151b684e5",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_147_Low_candle_shelf_stone_foot.uasset": "8a23cc0462c6a6d5876042a47827ee907f861338d49a39910cd271ecd5c43293",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_148_Low_candle_shelf_stone_foot_001.uasset": "62d22792f9c219fd81147ef372202c74980a2f346ae83f8e7c5dfd264aba0509",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_149_Meditation_room_sealed_sloping_wall_top.uasset": "cda4026b59086e130dc8122e6aeee83f607012fab2d04e152a72511041e790e1",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_150_Meditation_room_sealed_sloping_wall_top_001.uasset": "f5dc90901c70f6e0e70edb4141867ae2af5bb9c7adb4845be4f754a0a06e9c5d",
"unreal/SamfordMeditation/Content/Meditation/Meshes/MeditationRoom_SM_151_Meditation_room_south_wall_roof_junction.uasset": "a15c1f0b7ffedea36ebf741d1d015eadbc1aa7ddf470adb9383eb5989fad531c",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_00_Unpainted_hardwood___long_X_grain_BaseColor.uasset": "0b54b6a53fb415b245f012174587971eef472d510f3e83de77a0530efd002899",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_00_Unpainted_hardwood___long_X_grain_Normal.uasset": "3b6d37e0cb2b6d34e89cfcfb7aaead09e8e71dddd4c7e1a9e79dfbdab01a5a0b",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_01_Split_sandstone_facing___mineral_grain_BaseColor.uasset": "02c4738ca33c16ac71f42b875d59e744d167e6bdaf2d1a7df36a7656868f3f30",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_01_Split_sandstone_facing___mineral_grain_Normal.uasset": "bc8aedb4d7ba01b89b66db0a515153c84dd6c726441e38a7a2053355f1951629",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_03_Soft_warm_clay_plaster___natural_mineral_texture_BaseColor.uasset": "b75af5e1838e8cc5ef2d081b2915ce764b3b59636b9aaf73ddee77494fc39d67",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_03_Soft_warm_clay_plaster___natural_mineral_texture_Normal.uasset": "7388e1562d2123b288b153e94e58aa01fa2ddd0fad5c2420a6bb1ebaf3d93744",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_04_Oiled_spotted_gum_posts___vertical_grain_BaseColor.uasset": "12e918b5395ad667c4807b7e700d1a4a77e6dbee925fdfb0eed5acb22d15aa55",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_04_Oiled_spotted_gum_posts___vertical_grain_Normal.uasset": "cda2747a93ceb884fd7afd20346386aa8f3012aae6baf798741e59956ab113cd",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_06_Naturally_silvered_hardwood___horizontal_X_BaseColor.uasset": "ba9858b16cf3788a5cc83f6d4d0da8664eb042293c7b93cf17feb9a01f0362b6",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_06_Naturally_silvered_hardwood___horizontal_X_Normal.uasset": "67d8e12b7127f2ab8bae399edc81c5fee3a960ef7092d3590172b03e11ab6627",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_07_Bamboo_lining___golden_fibres_BaseColor.uasset": "ef28c317e0b6687db9a44319ff0b956bdf393ecf920909036e848ba9fee949b8",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_07_Bamboo_lining___golden_fibres_Normal.uasset": "80fcb18cfb2a67943667d88818a1819e6a30b35b5d933d3c559df05bdd7c8780",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_08_Pale_Colorbond___powder_coated_steel_BaseColor.uasset": "22ccbafc4e0f442563d3c0141c25ec29a7aa2a81e5acf83696f976585f61ca08",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_08_Pale_Colorbond___powder_coated_steel_Normal.uasset": "1f95b77a5552d28c152d918b00ff8615dc093d02b8b2dd217484655610324b55",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_09_Woven_natural_meditation_mat_BaseColor.uasset": "c13327e06d4b8b2f77394ab5dce1c05621aca98ff8ce6c1f39bae6037ecd1621",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_09_Woven_natural_meditation_mat_Normal.uasset": "f99699a1733e87e9b557e318063135892e9be942bf466d4d958cfa4e4135f66c",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_10_Undyed_flax_linen_BaseColor.uasset": "d9963bb610e7b82e42c2ab067564dba1138db6a1c251630d24d7b96124e90341",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_10_Undyed_flax_linen_Normal.uasset": "b5697990e41d95fc492ff0f07bf9f695a392e289f4c5ef20c1478768f6ad4140",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_11_Meditation_cushion_charcoal_linen_BaseColor.uasset": "e758abb71c9e652fb49edc84e0c61f6c9d4e581848aa893495a9c1c052ab3bea",
"unreal/SamfordMeditation/Content/Meditation/Textures/M_11_Meditation_cushion_charcoal_linen_Normal.uasset": "856541615db3e65038493a08ac035c13bfba146a50ffdcff1e282cf28a429c7c",
"unreal/SamfordMeditation/Content/Python/build_meditation.py": "e7db74037fb1b2c09a93f1689e24d99f7ed6079dabefc30c08c4b73fd45e6624",
"unreal/SamfordMeditation/SamfordMeditation.uproject": "d4fe01b8b79c30ef5b8ebef16941455feeed933b4fe6c81ab979cd7c12cd4ece",
"unreal/Textures/M_00_Unpainted_hardwood___long_X_grain_BaseColor.png": "fed3b9d752f07e05d251eb7b0fdfb84fce0a241bbde9b414644ec938b6f77055",
"unreal/Textures/M_00_Unpainted_hardwood___long_X_grain_Normal.png": "ced45dc260c8631e85df7735094d18abe67b26696f925b1bde15c8312b0a9904",
"unreal/Textures/M_01_Split_sandstone_facing___mineral_grain_BaseColor.png": "72b19da448476990fc810dafa2fcb72d8c486cc5d25f5094bcd02d81e9caea7a",
"unreal/Textures/M_01_Split_sandstone_facing___mineral_grain_Normal.png": "97b7978b88ecd5b32ebf68838c721870d20c93b2794c98f2e1c2d115de67937f",
"unreal/Textures/M_03_Soft_warm_clay_plaster___natural_mineral_texture_BaseColor.png": "c08c004e2a55ca9772558b96b19767fee7ad38ad9de2bc62b940c96fef9310ea",
"unreal/Textures/M_03_Soft_warm_clay_plaster___natural_mineral_texture_Normal.png": "cac53dbaf019cc54b235b8bd3c4a12fc706234215d492ef94e55b91ebdb81461",
"unreal/Textures/M_04_Oiled_spotted_gum_posts___vertical_grain_BaseColor.png": "3866322c322ab7543fa1c34452754633512da3b485f0acbf4d22eb5a3764bbff",
"unreal/Textures/M_04_Oiled_spotted_gum_posts___vertical_grain_Normal.png": "548b953d04c68a12251de712220b703f64719081f2e23cc2a08285ad61b0e9f0",
"unreal/Textures/M_06_Naturally_silvered_hardwood___horizontal_X_BaseColor.png": "6930cfe1bed3554fba432c16c206addc4fcb0738e398d9245a747e15c487e160",
"unreal/Textures/M_06_Naturally_silvered_hardwood___horizontal_X_Normal.png": "b96102a9f0fb704c2c23926176a2d0b387797f7d41ce897ee6df7135b9080974",
"unreal/Textures/M_07_Bamboo_lining___golden_fibres_BaseColor.png": "a5c68c806492f58f02b3fbd058164428600de55725760df65d582052cb7804f5",
"unreal/Textures/M_07_Bamboo_lining___golden_fibres_Normal.png": "82ed2f32a9de2830f287fcee29eea26218628b9af7da170159e10711fb19cb7b",
"unreal/Textures/M_08_Pale_Colorbond___powder_coated_steel_BaseColor.png": "9b62a7ed53c9cabb265923892c1fccad55801f2be80368706a49085b3e8dfe8c",
"unreal/Textures/M_08_Pale_Colorbond___powder_coated_steel_Normal.png": "e43f3d8a47487b69ec5333b35bb5a6e43c7a42b272a8c0d110367ef49242073a",
"unreal/Textures/M_09_Woven_natural_meditation_mat_BaseColor.png": "1e66a4f64e82c4553eaa5b71fdfb3687c629fd81640b22be898437ac1af31230",
"unreal/Textures/M_09_Woven_natural_meditation_mat_Normal.png": "45d7d605a99e9842f8b1bd694a0090082b3e2e519a4774b1ae2f90776534af07",
"unreal/Textures/M_10_Undyed_flax_linen_BaseColor.png": "d343b665185cbebcad8eeb788f77206dcb377101c962d60a9a06a27c8c809819",
"unreal/Textures/M_10_Undyed_flax_linen_Normal.png": "e13d5278f31eb79caf4d8d136af353e21fa47894519b2f78bb1632198600f252",
"unreal/Textures/M_11_Meditation_cushion_charcoal_linen_BaseColor.png": "41ea24a82506acba8965f06847aa4860c4d963d0bd8255b4905626809de38e90",
"unreal/Textures/M_11_Meditation_cushion_charcoal_linen_Normal.png": "4b0a80b762c24675c40ac26fd16272bd45039fa88098ada032631554da4dde57",
"unreal/meditation-room-manifest.json": "3a900693fd579ee176955618d889fce6f72632878e5ee566ff4061222af58318",
"unreal/unreal-scene-checks.json": "bed6fe7aea84a3dbe92dd382f970e7014e045365e38d05de12b69b933ffb634d",
"walkthroughs/UNREAL-CAMERA-ANIMATION.md": "f0c53352a5b6651409e99b6b696a95eca2330a8527fe347534efaabfe277b21a"
}
}

View File

0
site/lidar/raw/.gitkeep Normal file
View File

0
site/photos/.gitkeep Normal file
View File

0
site/surveys/.gitkeep Normal file
View File

View File

@@ -0,0 +1,13 @@
# Samford Valley adaptable homestead
Open `samford-modular-home.blend` in Blender. The main camera is a north-east aerial view; an alternative camera looks into the open living pavilion. Objects are grouped by building, future additions, services, landscape and planting. Materials are procedural, with no external texture downloads required.
Built scene: 12 × 6 m living pavilion, 8 × 5 m sleeping pavilion, exposed timber structure, six stacking glazed door panels, a 2.4 m north veranda, covered breezeway and accessible service pipes. The sleeping pavilion sits across a 4 m west gap and a 3 m south gap, interpreted as distances between the building edges.
Homestead additions: 18 roof-mounted photovoltaic panels on north-facing racks, a 12-panel ground array, battery/inverter cabinet, two 2 m diameter × 2 m tall poly rainwater tanks, six raised food beds, a glazed rectangular greenhouse and mature fruit trees.
Future office, guest pavilion and workshop are represented by translucent wire edges. Concrete pier stubs and post anchors are present at the office and guest locations. The workshop's north edge sits 15 m south of the living pavilion's south wall.
The accompanying SVG is a top-down solar and site layout. North is +Y; east is +X. Units are metres.
This is an imagined site and a visual design concept. Off-grid generation, battery storage, array spacing/shading and water yield have not been sized to household demand or a surveyed property.

View File

@@ -0,0 +1,440 @@
import bpy, math, random, os, sys, json
from mathutils import Vector
from math import sin, cos, pi
random.seed(418)
OUT=os.path.dirname(os.path.abspath(__file__))
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
for d in list(bpy.data.collections): bpy.data.collections.remove(d)
COL={}
for n in ['01 Living pavilion 12 x 6 m','02 Sleeping pavilion 8 x 5 m','03 Covered breezeway','04 Future modules - wire only','05 Water and services','06 Kitchen garden','07 Terrain and paths','08 Native planting','09 Eucalyptus and fruit trees','10 Furniture','11 Camera and atmosphere']:
c=bpy.data.collections.new(n); bpy.context.scene.collection.children.link(c); COL[n[:2]]=c
active='01'
def mesh(name,vs,fs,mat=None):
m=bpy.data.meshes.new(name); m.from_pydata(vs,[],fs); m.update(); o=bpy.data.objects.new(name,m); COL[active].objects.link(o)
if mat: m.materials.append(mat)
return o
cubevs=[(-.5,-.5,-.5),(-.5,-.5,.5),(-.5,.5,-.5),(-.5,.5,.5),(.5,-.5,-.5),(.5,-.5,.5),(.5,.5,-.5),(.5,.5,.5)]
cubefs=[(0,4,6,2),(1,3,7,5),(0,1,5,4),(2,6,7,3),(0,2,3,1),(4,5,7,6)]
cubefs=[tuple(reversed(f)) for f in cubefs]
def box(n,p,s,mat,bev=0):
o=mesh(n,[(v[0]*s[0],v[1]*s[1],v[2]*s[2]) for v in cubevs],cubefs,mat); o.location=p
if bev:
m=o.modifiers.new('Small construction edge','BEVEL'); m.width=bev; m.segments=2
o.modifiers.new('Corner normals','WEIGHTED_NORMAL')
return o
def beam(n,a,b,w,d,mat):
a,b=Vector(a),Vector(b); o=box(n,(a+b)/2,(w,d,(b-a).length),mat); o.rotation_euler=(b-a).to_track_quat('Z','Y').to_euler(); return o
def rod(n,a,b,r,mat,r2=None,sides=10):
a,b=Vector(a),Vector(b); vec=b-a; q=vec.to_track_quat('Z','Y'); r2=r if r2 is None else r2
vs=[tuple(a+q@Vector((cos(i*2*pi/sides)*rr,sin(i*2*pi/sides)*rr,z))) for z,rr in [(0,r),(vec.length,r2)] for i in range(sides)]
fs=[tuple(reversed(range(sides))),tuple(range(sides,sides*2))]+[(i,(i+1)%sides,(i+1)%sides+sides,i+sides) for i in range(sides)]
o=mesh(n,vs,fs,mat)
for p in o.data.polygons: p.use_smooth=len(p.vertices)==4
return o
def mat(n,c,rough=.65,metal=0):
m=bpy.data.materials.new(n); m.diffuse_color=(*c,1); m.use_nodes=True
p=m.node_tree.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*c,1); p.inputs['Roughness'].default_value=rough; p.inputs['Metallic'].default_value=metal
return m
def textured(n,c1,c2,scale,rough=.65,bump=.06):
m=mat(n,c1,rough); ns=m.node_tree.nodes; ls=m.node_tree.links; p=ns.get('Principled BSDF')
tex=ns.new('ShaderNodeTexCoord'); mp=ns.new('ShaderNodeVectorMath'); mp.operation='MULTIPLY'; mp.inputs[1].default_value=scale; ls.new(tex.outputs['Generated'],mp.inputs[0])
noise=ns.new('ShaderNodeTexNoise'); noise.inputs['Scale'].default_value=3; noise.inputs['Detail'].default_value=4; noise.inputs['Roughness'].default_value=.75; ls.new(mp.outputs[0],noise.inputs['Vector'])
ramp=ns.new('ShaderNodeValToRGB'); ramp.color_ramp.elements[0].position=.22; ramp.color_ramp.elements[0].color=(*c1,1); ramp.color_ramp.elements[1].position=.78; ramp.color_ramp.elements[1].color=(*c2,1); ls.new(noise.outputs['Fac'],ramp.inputs[0]); ls.new(ramp.outputs[0],p.inputs['Base Color'])
b=ns.new('ShaderNodeBump'); b.inputs['Strength'].default_value=.3; b.inputs['Distance'].default_value=bump; ls.new(noise.outputs['Fac'],b.inputs['Height']); ls.new(b.outputs[0],p.inputs['Normal'])
return m
timber=textured('Spotted gum - warm exposed structure',(.19,.095,.035),(.49,.29,.12),(75,75,1.5),.5,.012)
deckmat=textured('Spotted gum decking - longitudinal grain',(.17,.11,.055),(.39,.25,.115),(1.5,65,40),.65,.009)
weatherX=textured('Silvered hardwood weatherboard - X grain',(.19,.185,.158),(.43,.405,.335),(1.5,65,45),.76,.012)
weatherY=textured('Silvered hardwood weatherboard - Y grain',(.19,.185,.158),(.43,.405,.335),(65,1.5,45),.76,.012)
bamboo=textured('Bamboo lining - golden fibres',(.38,.25,.105),(.64,.48,.25),(65,1.5,35),.61,.008)
concrete=textured('Warm oxide polished concrete - fine aggregate',(.33,.275,.205),(.52,.46,.37),(65,65,65),.38,.006)
piermat=textured('Unsealed concrete footings',(.3,.32,.285),(.5,.51,.45),(12,12,12),.84,.018)
roofmat=textured('Pale Colorbond - powder coated steel',(.59,.60,.54),(.72,.72,.65),(70,70,70),.36,.42e-3)
roofmat.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.45
dark=mat('Blackened steel fixings',(.055,.06,.05),.4,.65)
glass=mat('Clear glazing',(.88,.94,.91),.07); glass.node_tree.nodes['Principled BSDF'].inputs['Transmission Weight'].default_value=1; glass.node_tree.nodes['Principled BSDF'].inputs['IOR'].default_value=1.45
soil=textured('Damp brown garden soil',(.045,.032,.018),(.12,.095,.05),(25,25,12),.95,.04)
gravelmat=textured('Loose local gravel - fine stones',(.22,.205,.155),(.45,.425,.33),(90,90,20),.91,.05)
tankmat=textured('Dark green polyethylene',(.035,.075,.052),(.067,.125,.08),(70,70,20),.48,.001)
copper=mat('Exposed copper service pipes',(.36,.17,.065),.36,.7)
fabric=textured('Natural linen upholstery',(.39,.37,.29),(.58,.55,.44),(90,90,90),.94,.003)
leafm=[mat('Leaf '+str(i),c,.68) for i,c in enumerate([(.08,.16,.065),(.14,.24,.085),(.21,.29,.11),(.11,.22,.15),(.25,.32,.14)])]
for m in leafm:
m.node_tree.nodes['Principled BSDF'].inputs['Subsurface Weight'].default_value=.06
bark=textured('Eucalyptus peeling pale bark',(.27,.26,.21),(.58,.57,.46),(18,18,1.1),.93,.025)
grassm=[mat('Native grass '+str(i),c) for i,c in enumerate([(.17,.22,.075),(.24,.29,.10),(.30,.30,.14),(.11,.19,.065)])]
debrism=[mat('Fallen leaves '+str(i),c) for i,c in enumerate([(.18,.09,.03),(.29,.19,.07),(.34,.29,.14)])]
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
F=1.12
def floor(cx,cy,w,d):
box('Insulated concrete floor slab',(cx,cy,F-.14),(w,d,.28),concrete,.025)
for x in [cx-w/2+.35,cx,cx+w/2-.35]:
for y in [cy-d/2+.35,cy+d/2-.35]:
z=ground(x,y); box('Concrete pier',(x,y,(z+F-.28)/2),(.35,.35,F-.28-z),piermat,.025)
box('Accessible galvanised post shoe',(x,y,F-.30),(.26,.26,.14),dark,.012)
def shell(cx,cy,w,d,northh,southh,fullopen):
floor(cx,cy,w,d)
yn=cy+d/2; ys=cy-d/2
# Exposed vertical posts; a simple layered wall body represents insulated panel construction.
box('South insulated timber wall',(cx,ys+.09,F+southh/2),(w,.18,southh),weatherX)
for i in range(int(southh/.15)):
box('South hardwood weatherboard',(cx,ys-.027,F+.075+i*.15),(w,.065,.142),weatherX,.004)
for x in [cx-w/2,cx+w/2]:
# Rectangular strips with sloped top ends, no ornamental geometry.
vs=[(x-.09,ys,F),(x-.09,yn,F),(x-.09,yn,F+northh),(x-.09,ys,F+southh),(x+.09,ys,F),(x+.09,yn,F),(x+.09,yn,F+northh),(x+.09,ys,F+southh)]
endwall=mesh('Insulated end wall',vs,[(0,3,2,1),(4,5,6,7),(0,4,7,3),(1,2,6,5),(2,3,7,6)],weatherY)
service_door=fullopen and x<cx
if service_door:
cutter=box('Temporary west door opening',(x,-1.8,F+1.15),(.6,1.12,2.3),None)
mod=endwall.modifiers.new('West entrance opening','BOOLEAN'); mod.operation='DIFFERENCE'; mod.object=cutter
bpy.context.view_layer.objects.active=endwall; endwall.select_set(True); bpy.ops.object.modifier_apply(modifier=mod.name); endwall.select_set(False); bpy.data.objects.remove(cutter,do_unlink=True)
for i in range(int(northh/.15)):
z=.075+i*.15
start=ys if z<=southh else ys+(z-southh)/(northh-southh)*d
if start<yn:
spans=[(start,yn)]
if service_door and z<2.3: spans=[(start,-2.36),(-1.24,yn)]
for aa,bb in spans:
if bb>aa: box('End wall silvered weatherboard',(x+(.115 if x>cx else -.115),(aa+bb)/2,F+z),(.065,bb-aa,.142),weatherY,.004)
if service_door:
for y in [-2.39,-1.21]: box('West entrance jamb',(x-.025,y,F+1.17),(.22,.065,2.34),timber)
box('West entrance lintel',(x-.025,-1.8,F+2.34),(.22,1.25,.08),timber)
for x in [cx-w/2,cx+w/2]:
for y,h in [(ys,southh),(yn,northh)]: box('Exposed spotted gum corner post',(x,y,F+h/2),(.125,.125,h),timber,.005)
if not fullopen:
# North facade assembled around generous bedroom glazing.
for x,ww in [(cx-w/2+.55,1.1),(cx+w/2-2.025,.75),(cx+w/2-.275,.55)]:
box('Bedroom north wall',(x,yn,F+northh/2),(ww,.19,northh),weatherX)
for x in [cx+w/2-1.63,cx+w/2-.57]: box('Bedroom breezeway entrance jamb',(x,yn,F+1.3),(.065,.22,2.6),timber)
box('Bedroom breezeway door lintel',(cx+w/2-1.1,yn,F+2.63),(1.13,.22,.08),timber)
box('Bedroom north lintel',(cx,yn,F+northh-.22),(w,.20,.44),weatherX)
for x in [cx-w/2+i*.60 for i in range(int(w/.60)+1)]:
beam('Exposed roof rafter',(x,ys-.28,F+southh-.09),(x,yn+.28,F+northh-.09),.065,.19,timber)
for i in range(int(w/.14)):
x=cx-w/2+.07+i*.14
beam('Bamboo ceiling lining board',(x,ys,F+southh+.015),(x,yn,F+northh+.015),.135,.019,bamboo)
return yn,ys
def roof(cx,cy,w,d,nh,sh,over=.4):
yn=cy+d/2+over; ys=cy-d/2-over
def z(y): return F+sh+(y-(cy-d/2))/d*(nh-sh)+.14
nx=int((w+2*over)/.09)*8; vs=[]
for i in range(nx+1):
x=cx-w/2-over+(w+2*over)*i/nx
for y in [ys,yn]: vs.append((x,y,z(y)+.011*sin(i*2*pi/8)))
mesh('Continuous corrugated pale metal roof',vs,[(2*i,2*i+2,2*i+3,2*i+1) for i in range(nx)],roofmat)
for x in [cx-w/2-over,cx+w/2+over]: beam('Plain roof edge flashing',(x,ys,z(ys)),(x,yn,z(yn)),.05,.09,roofmat)
for y in [ys,yn]: box('Roof edge',(cx,y,z(y)),(w+2*over,.06,.09),roofmat)
box('Rectangular low edge gutter',(cx,ys-.025,z(ys)-.035),(w+2*over,.16,.12),roofmat)
for y in [cy-d/2+i*1.2 for i in range(int(d/1.2)+1)]: box('Visible timber purlin',(cx,y,z(y)-.095),(w,.07,.075),timber)
def glazing(cx,y,width,height,bottom=F,stack=1):
for i in range(stack):
yy=y+i*.10; box('Sliding glass pane',(cx,yy,bottom+height/2),(width-.10,.016,height-.10),glass)
for x in [cx-width/2,cx+width/2]: box('Spotted gum sliding door stile',(x,yy,bottom+height/2),(.055,.070,height),timber,.004)
for z in [bottom+.035,bottom+height-.035]: box('Sliding door top and bottom rail',(cx,yy,z),(width,.07,.07),timber,.004)
box('Recessed door pull',(cx-width/2+.09,yy+.045,bottom+1.1),(.025,.025,.22),dark,.003)
box('Exposed door track',(cx,y-.05,bottom-.01),(width+.05,.15,.026),dark)
active='01'; shell(0,0,12,6,3.4,2.7,True); roof(0,0,12,6,3.4,2.7)
box('North opening full width header',(0,3,F+3.24),(12,.20,.23),timber)
box('Six panel full width sliding track',(0,3.015,F+.014),(11.85,.65,.03),dark)
glazing(4.97,3.04,1.92,3.09,stack=6)
# 2.4 m veranda. The canopy follows the same practical roof language, slightly falling north.
floor(0,4.2,12,2.4)
for x in [-5.93,-2,2,5.93]:
box('125 x 125 mm spotted gum veranda post',(x,5.32,F+1.47),(.125,.125,2.94),timber,.005)
box('Veranda post base shoe',(x,5.32,F+.065),(.15,.15,.13),dark,.006)
for z in [F+.1,F+2.84]: rod('Visible structural bolt',(x-.07,5.32,z),(x+.07,5.32,z),.012,dark,sides=8)
box('Veranda exposed front beam',(0,5.32,F+2.91),(12.1,.125,.22),timber)
for x in [-5.9+i*.6 for i in range(21)]: beam('Veranda visible rafter',(x,2.98,F+3.27),(x,5.6,F+2.92),.065,.17,timber)
roof(0,4.2,12,2.4,2.95,3.28,over=.20)
for i in range(4): box('Loose wide entry step',(0,5.7+i*.40,F-.22-i*.22),(3.6,.43,.17),concrete,.015)
active='02'; shell(-14,-8.5,8,5,3.4,2.7,False); roof(-14,-8.5,8,5,3.4,2.7)
glazing(-14.45,-5.98,4.65,2.95,stack=1)
box('Bedroom bathroom partition',(-11.8,-8.8,F+1.35),(.12,4.35,2.7),bamboo)
box('Bed timber base',(-14.7,-8.65,F+.23),(2.0,2.25,.40),timber,.02)
box('Bed linen mattress',(-14.7,-8.6,F+.51),(1.9,2.15,.20),fabric,.07)
for x in [-15.2,-14.25]: box('Linen pillow',(x,-9.3,F+.68),(.7,.4,.13),fabric,.07)
# Breezeway terminates at the bedroom north entrance and living west service entrance.
active='03'
for cx,cy,w,d in [(-8.6,-1.8,5.2,1.6),(-11.2,-3.9,1.6,5.8)]:
for i in range(int(w/.14)):
box('Breezeway removable deck board',(cx-w/2+.07+i*.14,cy,F-.03),(.132,d,.065),deckmat,.003)
box('Breezeway floor bearer',(cx,cy,F-.18),(w,d,.20),timber)
box('Breezeway rectangular metal canopy',(cx,cy,F+2.68),(w+.24,d+.24,.07),roofmat,.012)
for x,y in [(cx-w/2+.1,cy-d/2+.1),(cx+w/2-.1,cy+d/2-.1)]:
box('Breezeway post',(x,y,F+1.3),(.125,.125,2.6),timber,.004)
z=ground(x,y); box('Breezeway pier',(x,y,(F+z)/2),(.26,.26,F-z),piermat,.01)
active='10'
# One undivided room: kitchen south-west, dining centre, loose furniture toward valley.
for x in [-4.8,-3.6,-2.4,-1.2]:
box('Simple timber kitchen cabinet',(x,-2.52,F+.44),(1.16,.66,.88),deckmat,.012)
box('Flat cabinet pull',(x,-2.175,F+.76),(.27,.025,.02),dark)
box('Continuous kitchen worktop',(-3,-2.52,F+.93),(5.1,.75,.055),concrete,.012)
box('Stainless sink',(-4.2,-2.5,F+.965),(.70,.48,.025),dark,.04)
rod('Exposed kitchen tap',(-4.2,-2.75,F+.97),(-4.2,-2.75,F+1.3),.018,copper)
rod('Tap spout',(-4.2,-2.75,F+1.3),(-4.2,-2.48,F+1.3),.018,copper)
box('Induction cooktop',(-1.4,-2.52,F+.969),(.6,.52,.019),dark,.01)
box('Open timber kitchen shelf',(-3,-2.75,F+1.83),(4.7,.30,.045),deckmat,.007)
for x in [-4.6,-3.9,-3.1]: rod('Pantry ceramic jar',(x,-2.71,F+1.86),(x,-2.71,F+2.05),.085,concrete,sides=20)
box('Dining table',(-.2,.3,F+.76),(2.3,1.05,.07),deckmat,.018)
for x in [-1.15,.75]:
for y in [-.10,.70]: box('Dining table square leg',(x,y,F+.37),(.07,.07,.74),timber,.006)
for x in [-.9,.45]:
for y in [-.6,1.2]:
box('Timber dining chair seat',(x,y,F+.43),(.46,.46,.045),deckmat,.008)
for dx in [-.17,.17]:
for dy in [-.17,.17]: box('Chair leg',(x+dx,y+dy,F+.21),(.038,.038,.42),timber)
box('Chair back',(x,y+(.2 if y>0 else -.2),F+.69),(.46,.035,.32),deckmat,.008)
box('Loose linen sofa base',(3.2,-.6,F+.22),(2.3,.93,.35),timber,.025)
box('Linen sofa cushion',(3.2,-.55,F+.46),(2.22,.90,.22),fabric,.07)
box('Linen sofa back',(3.2,-.94,F+.73),(2.3,.17,.69),fabric,.065)
box('Low rectangular coffee table',(3.1,.85,F+.33),(1.35,.65,.055),deckmat,.015)
for x in [2.58,3.62]: box('Coffee table support',(x,.85,F+.17),(.065,.52,.30),timber)
for x in [-4.5,-3.3]:
box('Veranda bench seat',(x,4.45,F+.42),(.8,.6,.06),deckmat,.014)
for dx in [-.3,.3]: box('Veranda bench leg',(x+dx,4.45,F+.21),(.06,.48,.4),timber)
active='05'
for cx,cy in [(-18.95,-8.1),(-18.95,-10.6)]:
z=ground(cx,cy); box('Gravel tank pad',(cx,cy,z+.05),(2.3,2.3,.1),gravelmat)
rod('2000 mm dark green poly rainwater tank',(cx,cy,z+.1),(cx,cy,z+2.1),1,tankmat,sides=64)
rod('Poly tank flat top',(cx,cy,z+2.1),(cx,cy,z+2.14),.96,tankmat,sides=64)
rod('Tank service lid',(cx+.2,cy,z+2.14),(cx+.2,cy,z+2.2),.24,tankmat,sides=32)
for j in range(12):
zz=z+.23+j*.15
bpy.ops.mesh.primitive_torus_add(major_segments=48,minor_segments=6,major_radius=1.005,minor_radius=.014,location=(cx,cy,zz))
o=bpy.context.object; o.name='Poly tank reinforcing rib'; o.data.materials.append(tankmat)
for c in list(o.users_collection): c.objects.unlink(o)
COL[active].objects.link(o)
rod('Accessible tank outlet',(cx+.93,cy,z+.28),(cx+1.25,cy,z+.28),.04,copper)
for x,y,z in [(-5.9,-3.43,F+2.75),(-17.9,-11.43,F+2.75)]:
rod('Exposed rainwater downpipe',(x,y,z),(x,y,ground(x,y)+.22),.048,roofmat)
for zz in [F+.28,F+.4]: rod('Surface mounted accessible services',(-5.98,-3.15,zz),(5.9,-3.15,zz),.018,copper)
active='04'
ghost=mat('Future volume - translucent fine line',(.53,.73,.73),.4)
ns=ghost.node_tree.nodes; ls=ghost.node_tree.links; ns.clear(); out=ns.new('ShaderNodeOutputMaterial'); tr=ns.new('ShaderNodeBsdfTransparent'); em=ns.new('ShaderNodeEmission'); em.inputs[0].default_value=(.48,.68,.65,1); em.inputs[1].default_value=.6; mix=ns.new('ShaderNodeMixShader'); mix.inputs[0].default_value=.55; ls.new(tr.outputs[0],mix.inputs[1]); ls.new(em.outputs[0],mix.inputs[2]); ls.new(mix.outputs[0],out.inputs['Surface'])
def future(name,cx,cy,w,d,piers):
z=max(ground(cx,cy)+.38,F); low=z+2.7; high=z+3.4
pts=[(cx-w/2,cy-d/2,z),(cx+w/2,cy-d/2,z),(cx+w/2,cy+d/2,z),(cx-w/2,cy+d/2,z),(cx-w/2,cy-d/2,low),(cx+w/2,cy-d/2,low),(cx+w/2,cy+d/2,high),(cx-w/2,cy+d/2,high)]
for a,b in [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)]: beam(name+' - ghost edge',pts[a],pts[b],.026,.026,ghost)
# Sparse secondary lines maintain a visibly wireframe-only future volume.
for t in [.33,.66]:
x=cx-w/2+w*t; beam(name+' - future roof line',(x,cy-d/2,low),(x,cy+d/2,high),.013,.013,ghost)
if piers:
for x in [cx-w/2+.3,cx+w/2-.3]:
for y in [cy-d/2+.3,cy+d/2-.3]:
g=ground(x,y); box(name+' - installed concrete pier stub',(x,y,g+.16),(.35,.35,.32),piermat,.015); box('Future post anchor',(x,y,g+.34),(.15,.15,.075),dark)
future('Future office',11.3,-.6,7,5,True)
future('Future guest pavilion',-25,-8.5,7,5,True)
future('Future workshop',0,-21,8,6,False)
active='07'
terrainmat=textured('Living soil and patchy subtropical ground',(.09,.13,.035),(.22,.25,.095),(18,18,7),.98,.07)
N=150; vs=[]
for j in range(N+1):
y=-85+j*170/N
for i in range(N+1):
x=-95+i*190/N; vs.append((x,y,ground(x,y)))
ob=mesh('Continuous gently north falling hillside',vs,[(j*(N+1)+i,j*(N+1)+i+1,(j+1)*(N+1)+i+1,(j+1)*(N+1)+i) for j in range(N) for i in range(N)],terrainmat)
for p in ob.data.polygons: p.use_smooth=True
def path(n,points,width):
vs=[]
for idx,(x,y) in enumerate(points):
prev=Vector(points[max(0,idx-1)]); nex=Vector(points[min(len(points)-1,idx+1)]); tangent=(nex-prev).normalized(); normal=Vector((-tangent.y,tangent.x))
for side in [-1,1]:
xx=x+normal.x*width/2*side; yy=y+normal.y*width/2*side; vs.append((xx,yy,ground(xx,yy)+.028))
mesh(n,vs,[(2*i,2*i+1,2*i+3,2*i+2) for i in range(len(points)-1)],gravelmat)
path('Unsealed gravel arrival and module connection',[(1,14),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],1.5)
path('Kitchen garden gravel trail',[(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2)],1.1)
active='06'
beds=[]
for x in [17.8,21.2]:
for y in [-2.7,.6,3.9]:
beds.append((x,y)); z=ground(x,y)
for xx in [x-1.15,x+1.15]: box('Raised bed end board',(xx,y,z+.30),(.055,1.25,.54),weatherY,.005)
for yy in [y-.625,y+.625]: box('Raised bed long board',(x,yy,z+.30),(2.35,.055,.54),weatherX,.005)
box('Raised vegetable bed soil',(x,y,z+.43),(2.23,1.15,.14),soil)
# Efficient batched mesh planting. Individual narrow leaves catch the grazing sunlight.
active='08'
gv=[]; gf=[]; gi=[]
def blade(x,y,z,ang,length,width,lean,mi):
k=len(gv); side=Vector((cos(ang),sin(ang),0)); forward=Vector((-sin(ang),cos(ang),0))
base=Vector((x,y,z)); mid=base+forward*lean*.38+Vector((0,0,length*.65)); tip=base+forward*lean+Vector((0,0,length*.83))
for p in [base-side*width*.25,base+side*width*.25,mid-side*width*.5,mid+side*width*.5,tip]: gv.append(tuple(p))
gf.extend([(k,k+1,k+3,k+2),(k+2,k+3,k+4)]); gi.extend([mi,mi])
def forbidden(x,y):
return (-6.5<x<6.5 and -3.6<y<6.7) or (-18.4<x<-9.5 and -11.6<y<-5.5) or (-12.3<x<-5.7 and -6.6<y<-.8) or (16<x<23 and -4<y<5) or (-20.2<x<-17.7 and -12<y<-6.5)
for i in range(14000):
x=random.uniform(-46,39); y=random.uniform(-36,37)
if forbidden(x,y): continue
# Keep gravel paths readable and thin the immediate forecourt.
if (abs(x)<1.1 and 6<y<15) or (-4<x<16 and 6.7<y<8.8): continue
if random.random()<.35 and -8<x<8 and y>6: continue
z=ground(x,y); ln=random.uniform(.12,.42)
for j in range(5): blade(x+random.uniform(-.09,.09),y+random.uniform(-.09,.09),z,random.uniform(0,2*pi),ln*random.uniform(.7,1.2),random.uniform(.012,.025),random.uniform(.08,.25),random.randrange(4))
for x,y in [(-6.9,3),(-7,4.4),(6.7,3),(7.2,4.6),(-17.4,-4.9),(-16,-4.7),(-14,-4.6),(-8.5,-7),(3.7,6.4),(5.4,6.3),(15.8,5.6),(22.8,6)]:
for j in range(85): blade(x+random.uniform(-.2,.2),y+random.uniform(-.2,.2),ground(x,y),random.uniform(0,2*pi),random.uniform(.5,.95),random.uniform(.016,.035),random.uniform(.3,.65),random.randrange(4))
o=mesh('Native grasses and lomandra - individual blade geometry',gv,gf)
for m in grassm: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,gi): p.material_index=idx
# Ground leaf litter: bent lanceolate eucalyptus leaves.
dv=[]; df=[]; di=[]
for i in range(4200):
x=random.uniform(-35,30); y=random.uniform(-24,22)
if forbidden(x,y): continue
z=ground(x,y)+.035; a=random.uniform(0,2*pi); L=random.uniform(.06,.16); W=L*.23; k=len(dv)
for u,v,dz in [(-L,0,0),(0,W,.008),(L,0,.015),(0,-W,.008)]: dv.append((x+u*cos(a)-v*sin(a),y+u*sin(a)+v*cos(a),z+dz))
df.append((k,k+1,k+2,k+3)); di.append(random.randrange(3))
o=mesh('Scattered fallen eucalyptus leaves',dv,df)
for m in debrism: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,di): p.material_index=idx
for i in range(55):
x=random.uniform(-33,28); y=random.uniform(-20,20)
if not forbidden(x,y): rod('Fallen twig',(x,y,ground(x,y)+.03),(x+random.uniform(.15,.65),y+.25,ground(x,y)+.055),.012,bark,r2=.007,sides=5)
active='09'
def tree(cx,cy,h,fruit=False):
z=ground(cx,cy); lean=random.uniform(-.6,.6); trunkh=h*(.53 if fruit else .68)
base=Vector((cx,cy,z)); top=Vector((cx+lean,cy+.15,z+trunkh))
rod('Fruit tree trunk' if fruit else 'Eucalyptus pale trunk',base,top,h*.022,bark,r2=h*.007,sides=14)
lv=[]; lf=[]; li=[]
clusters=20 if fruit else 24
for j in range(clusters):
a=j*2.4; rr=random.uniform(.15,.30)*h; zz=z+h*random.uniform(.62,.92)
end=Vector((cx+cos(a)*rr,cy+sin(a)*rr,zz))
source=base.lerp(top,random.uniform(.55,.98)); elbow=source.lerp(end,.56)+Vector((0,0,.22))
rod('Exposed canopy branch',source,elbow,h*.008,bark,r2=h*.004,sides=8); rod('Fine canopy branch',elbow,end,h*.004,bark,r2=.015,sides=6)
for k in range(170 if fruit else 135):
theta=random.uniform(0,2*pi); u=random.uniform(-1,1); rad=random.random()**(1/3); ss=math.sqrt(1-u*u)
center=end+Vector((cos(theta)*ss*rad*h*.105,sin(theta)*ss*rad*h*.105,u*rad*h*.071))
a2=random.uniform(0,2*pi); length=random.uniform(.10,.22) if not fruit else random.uniform(.10,.17); width=length*(.20 if not fruit else .45)
long=Vector((cos(a2),sin(a2),random.uniform(-1.2,.2))).normalized()*length
side=Vector((-sin(a2),cos(a2),.05))*width
idx=len(lv)
for p in [center-long,center+side,center+long,center-side,center+Vector((0,0,.016))]: lv.append(tuple(p))
lf.extend([(idx,idx+1,idx+4),(idx+1,idx+2,idx+4),(idx+2,idx+3,idx+4),(idx+3,idx,idx+4)]); li.extend([random.randrange(5)]*4)
o=mesh(('Mango / avocado' if fruit else 'Eucalyptus')+' individual canopy leaves',lv,lf)
for m in leafm: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,li): p.material_index=idx
for x,y,h in [(-31,3,14),(-26,8,15),(-21,12,16),(-34,-15,16),(-23,-22,14),(-14,-23,16),(9,-18,15),(18,-17,16),(28,-10,14),(29,10,14),(35,0,17),(-36,20,18),(34,24,16),(-8,-33,17),(20,-31,18),(-39,-29,19),(38,-29,19)]: tree(x,y,h)
for x,y,h in [(18,9.6,6.5),(24,8.6,7),(25,-4.7,6)]: tree(x,y,h,True)
# Bed crops reuse the same individual blades at a smaller scale.
active='06'; gv=[]; gf=[]; gi=[]
for x,y in beds:
z=ground(x,y)+.52
for dx in [-.8,-.4,0,.4,.8]:
for dy in [-.33,.33]:
for k in range(13): blade(x+dx,y+dy,z,random.uniform(0,2*pi),random.uniform(.14,.31),.045,random.uniform(.08,.2),random.randrange(4))
o=mesh('Kitchen garden leafy vegetables',gv,gf)
for m in leafm[:4]: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,gi): p.material_index=idx
active='05'
pv=mat('Solar photovoltaic cells - blue black silicon',(.014,.026,.043),.23,.42)
silver=mat('Solar anodised aluminium frames',(.38,.42,.42),.3,.8)
cellline=mat('Solar cell fine contacts',(.18,.24,.29),.4,.65)
def solar_panel(cx,cy,z,w=1.10,d=1.76):
# North facing rack: high south edge, low north edge, clearly separate from roof pitch.
tilt=math.radians(20); cs=cos(tilt); sn=sin(tilt)
def point(x,y,zz=0): return (cx+x,cy+y*cs,z-y*sn+zz)
mesh('PV panel glass and cells',[point(-w/2,-d/2),point(w/2,-d/2),point(w/2,d/2),point(-w/2,d/2)],[(0,1,2,3)],pv)
for x in [-w/2,w/2]: beam('PV module side frame',point(x,-d/2),point(x,d/2),.027,.035,silver)
for y in [-d/2,d/2]: beam('PV module end frame',point(-w/2,y),point(w/2,y),.027,.035,silver)
for i in range(1,6): beam('Photovoltaic cell columns',point(-w/2+w*i/6,-d/2,.006),point(-w/2+w*i/6,d/2,.006),.003,.003,cellline)
for j in range(1,11): beam('Photovoltaic cell rows',point(-w/2,-d/2+d*j/11,.006),point(w/2,-d/2+d*j/11,.006),.003,.003,cellline)
for y in [-1.38,1.30]:
z=F+2.7+(y+3)/6*.7+.91
for i in range(9):
x=-4.82+i*1.205; solar_panel(x,y,z)
for yy in [y-.64,y+.64]:
rz=F+2.7+(yy+3)/6*.7+.16; pz=z-(yy-y)*math.tan(math.radians(20))-.04
beam('Roof solar rack standoff',(x,yy,rz),(x,yy,pz),.045,.045,silver)
for yy in [y-.58,y+.58]:
zz=z-(yy-y)*math.tan(math.radians(20))-.055; beam('Continuous solar mounting rail',(-5.5,yy,zz),(5.5,yy,zz),.045,.06,silver)
for y in [-10.3,-13.0]:
for i in range(6):
x=7.2+i*1.21; z=ground(x,y)+1.28; solar_panel(x,y,z)
for yy in [y-.65,y+.65]:
beam('Ground array removable steel leg',(x,yy,ground(x,yy)),(x,yy,z-(yy-y)*math.tan(math.radians(20))-.04),.055,.055,dark)
box('Off-grid battery cabinet',(6.42,-1.7,F+.74),(.58,1.06,1.48),roofmat,.025)
box('Battery cabinet service access',(6.73,-1.7,F+.74),(.022,.94,1.31),dark,.018)
box('Solar inverter',(6.38,-.68,F+1.45),(.42,.46,.63),roofmat,.018)
for yy in [-.78,-.61]: rod('Accessible inverter conduit',(6.42,yy,F+1.11),(6.42,yy,F+.22),.019,copper)
active='06'
gx,gy=18.2,-8.5; gz=ground(gx,gy)+.14; gw,gd=4.3,5
box('Greenhouse loose gravel base',(gx,gy,gz-.055),(gw+.4,gd+.4,.11),gravelmat)
# Rectangular lean-to greenhouse, with a simple mono-pitch glazed roof.
for x in [gx-gw/2+i*gw/4 for i in range(5)]:
for y,h in [(gy-gd/2,2.7),(gy+gd/2,2.4)]: box('Greenhouse timber upright',(x,y,gz+h/2),(.065,.065,h),timber,.003)
for y in [gy-gd/2+i*gd/5 for i in range(6)]:
h=2.7-(y-(gy-gd/2))/gd*.3
for x in [gx-gw/2,gx+gw/2]: box('Greenhouse side upright',(x,y,gz+h/2),(.065,.065,h),timber,.003)
beam('Greenhouse exposed roof bar',(gx-gw/2,y,gz+h),(gx+gw/2,y,gz+h),.065,.075,timber)
for x in [gx-gw/2,gx+gw/2]:
for z in [gz+.07,gz+1.15]: box('Greenhouse side rail',(x,gy,z),(.065,gd,.065),timber)
mesh('Greenhouse side glazing',[(x,gy-gd/2,gz+.08),(x,gy+gd/2,gz+.08),(x,gy+gd/2,gz+2.4),(x,gy-gd/2,gz+2.7)],[(0,1,2,3)],glass)
for y,h in [(gy-gd/2,2.7),(gy+gd/2,2.4)]:
box('Greenhouse end glazing',(gx,y,gz+h/2),(gw,.012,h),glass)
for z in [gz+.07,gz+1.15,gz+h]: box('Greenhouse end rail',(gx,y,z),(gw,.065,.065),timber)
mesh('Greenhouse mono-pitch glass roof',[(gx-gw/2,gy-gd/2,gz+2.7),(gx+gw/2,gy-gd/2,gz+2.7),(gx+gw/2,gy+gd/2,gz+2.4),(gx-gw/2,gy+gd/2,gz+2.4)],[(0,1,2,3)],glass)
for x in [gx-gw/2+i*gw/4 for i in range(5)]: beam('Greenhouse sloping roof rafter',(x,gy-gd/2,gz+2.7),(x,gy+gd/2,gz+2.4),.065,.07,timber)
for x in [gx-1.4,gx+1.4]:
box('Greenhouse growing bed',(x,gy,gz+.27),(.85,4.4,.48),weatherY,.01)
box('Greenhouse growing soil',(x,gy,gz+.52),(.78,4.3,.06),soil)
for y in [gy-1.6,gy-.8,gy,gy+.8,gy+1.6]:
rod('Greenhouse tomato stake',(x,y,gz+.55),(x,y,gz+1.95),.015,timber,sides=6)
for zz in [gz+.75,gz+1.0,gz+1.25,gz+1.5]:
for s in [-1,1]:
beam('Climbing food plant stem',(x,y,zz),(x+s*.22,y+.08,zz+.10),.018,.018,leafm[1])
mesh('Greenhouse broad vegetable leaf',[(x,y,zz),(x+s*.16,y-.1,zz+.1),(x+s*.32,y,zz+.08),(x+s*.16,y+.13,zz+.12)],[(0,1,2,3)],leafm[1])
box('Greenhouse door handle',(gx+.43,gy+gd/2+.045,gz+1.1),(.025,.05,.22),dark)
for x in [gx-.48,gx+.48]: box('Greenhouse door frame',(x,gy+gd/2+.025,gz+1.1),(.065,.065,2.2),timber)
path('Greenhouse gravel access',[(20,2),(20,-4.6),(18.2,-5.7)],1.0)
active='11'; scene=bpy.context.scene
world=bpy.data.worlds.new('Subtropical early morning sky'); world.use_nodes=True; scene.world=world
ns=world.node_tree.nodes; ls=world.node_tree.links; sky=ns.new('ShaderNodeTexSky'); sky.sky_type='MULTIPLE_SCATTERING'; sky.sun_elevation=math.radians(12); sky.sun_rotation=math.radians(45); sky.altitude=.12; sky.air_density=1.1; sky.aerosol_density=1.8; sky.sun_disc=False; ls.new(sky.outputs[0],ns['Background'].inputs[0]); ns['Background'].inputs[1].default_value=.22
bpy.ops.object.light_add(type='SUN',location=(35,40,30)); sun=bpy.context.object; sun.name='Warm early morning sun from north-east'; sun.data.energy=2.2; sun.data.angle=math.radians(3); sun.data.color=(1,.74,.45); sun.rotation_euler=(Vector((-15,-20,0))-sun.location).to_track_quat('-Z','Y').to_euler()
vol=bpy.data.materials.new('Very light canopy haze'); vol.use_nodes=True; ns=vol.node_tree.nodes; ns.clear(); v=ns.new('ShaderNodeVolumePrincipled'); v.inputs['Density'].default_value=.0017; v.inputs['Color'].default_value=(.72,.78,.71,1); v.inputs['Anisotropy'].default_value=.35; out=ns.new('ShaderNodeOutputMaterial'); vol.node_tree.links.new(v.outputs['Volume'],out.inputs['Volume']); box('Subtle atmosphere over hillside',(0,0,15),(170,170,38),vol)
bpy.ops.object.camera_add(location=(43,58,30)); cam=bpy.context.object; cam.name='NE aerial - complete adaptable compound'; target=Vector((-5,-6,1.0)); cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler(); cam.data.type='PERSP'; cam.data.lens=46; cam.data.clip_end=350; scene.camera=cam
# A close exterior camera is included for inspecting the open room and exposed structure.
bpy.ops.object.camera_add(location=(16,23,9)); detail=bpy.context.object; detail.name='Alternate - living pavilion and open veranda'; detail.rotation_euler=(Vector((-1,0,2.2))-detail.location).to_track_quat('-Z','Y').to_euler(); detail.data.lens=43
for o in [sun,cam,detail]:
for c in list(o.users_collection): c.objects.unlink(o)
COL['11'].objects.link(o)
scene.render.engine='CYCLES'; scene.cycles.samples=96; scene.cycles.use_denoising=True; scene.cycles.adaptive_threshold=.025
try:
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
devices=[d for d in prefs.devices if d.type=='METAL']
if devices:
for d in prefs.devices: d.use=d.type=='METAL'
scene.cycles.device='GPU'
print('CYCLES_DEVICES',[(d.name,d.type,d.use) for d in prefs.devices],flush=True)
except Exception as e: print('CPU rendering',e,flush=True)
scene.cycles.max_bounces=8; scene.cycles.transmission_bounces=6; scene.cycles.transparent_max_bounces=8; scene.cycles.volume_bounces=1
scene.render.resolution_x=2560; scene.render.resolution_y=1440; scene.render.resolution_percentage=100; scene.render.image_settings.file_format='PNG'; scene.render.film_transparent=False
scene.view_settings.view_transform='AgX'; scene.view_settings.look='AgX - Medium High Contrast'; scene.view_settings.exposure=.3
scene.unit_settings.system='METRIC'; scene.unit_settings.length_unit='METERS'
scene['Design intent']='Start with two insulated rectangles; live in them first; adapt. Exposed structure and services, no ornament.'
scene['Orientation']='X east, Y north, Z up. Living 12 x 6 m, clear internal roof height north 3.4 m / south 2.7 m. North veranda 2.4 m.'
scene['Site layout assumption']='Sleeping pavilion 8 x 5 m, 4 m clear west gap and 3 m clear south gap from living. Future workshop north edge 15 m south of living south edge.'
scene['Geographic context']='Imagined Samford Valley site, Queensland. Not a survey or a construction design.'
scene['Homestead energy concept']='18 rooftop PV modules on north-facing racks; 12 ground-array modules; accessible battery cabinet and inverter. Visual layout only, system capacity not engineered.'
scene['Food and water']='Six raised vegetable beds; rectangular glazed greenhouse with growing beds; mature fruit trees; two 2 m diameter x 2 m high poly rainwater tanks.'
readme=bpy.data.texts.new('READ ME - Adaptable Samford home'); readme.write('SAMFORD VALLEY / MODULAR HOME\n\nAll buildings are predictions. All predictions are wrong. — Stewart Brand\n\nTwo insulated timber rectangles, north-opening living space and covered breezeway. Structural and service elements are intentionally accessible. Future office, guest and workshop are translucent edges only; office and guest pier stubs are installed.\n\nMetric units. X east, Y north. All materials are procedural and contained in this file. Two cameras included. Main camera: NE aerial.\nSleeping offsets interpreted as clear gaps between building edges.\n')
bpy.ops.object.select_all(action='DESELECT')
for screen in bpy.data.screens:
for area in screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.region_3d.view_perspective='CAMERA'; area.spaces.active.shading.type='MATERIAL'
scene.render.filepath=os.path.join(OUT,'samford-modular-final.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
# A quick first pass checks framing before committing to the full Cycles render.
scene.render.resolution_percentage=45; scene.cycles.samples=24; scene.render.filepath=os.path.join(OUT,'samford-modular-draft.png')
bpy.ops.render.render(write_still=True)
print('DRAFT_COMPLETE',len(scene.objects),flush=True)

View File

@@ -0,0 +1,7 @@
import bpy,os
s=bpy.context.scene
s.render.engine='CYCLES'; s.cycles.device='CPU'; s.cycles.samples=8; s.cycles.use_denoising=True
s.render.resolution_x=960; s.render.resolution_y=540; s.render.resolution_percentage=100
s.render.filepath=os.path.join(os.path.dirname(__file__),'framing-check.png')
bpy.data.objects['Subtle atmosphere over hillside'].hide_render=True
bpy.ops.render.render(write_still=True)

View File

@@ -0,0 +1,49 @@
from pathlib import Path
out=Path(__file__).parent
parts=['''<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1050" viewBox="0 0 1400 1050"><rect width="1400" height="1050" fill="#f5f3eb"/><style>text{font-family:Arial,sans-serif;fill:#203e32}.label{font-size:17px;font-weight:600}.small{font-size:14px;fill:#627368}.note{font-size:16px}.future{fill:none;stroke:#76978d;stroke-width:2;stroke-dasharray:7 5}</style><text x="70" y="70" font-size="34" font-weight="600">SAMFORD VALLEY</text><text x="70" y="104" font-size="21">Adaptable homestead · solar and site layout</text><path d="M1290 135V65m-9 15 9-15 9 15" stroke="#203e32" fill="none" stroke-width="2"/><text x="1283" y="52" font-size="19">N</text>''']
S=19
def xy(x,y): return (70+(x+31)*S,170+(13-y)*S)
def rect(cx,cy,w,d,fill,stroke='#52614d',css=''):
x,y=xy(cx-w/2,cy+d/2); parts.append(f'<rect x="{x}" y="{y}" width="{w*S}" height="{d*S}" fill="{fill}" stroke="{stroke}" stroke-width="1.5" class="{css}"/>')
def text(x,y,t,cl='label',anchor='middle'):
xx,yy=xy(x,y); parts.append(f'<text x="{xx}" y="{yy}" text-anchor="{anchor}" class="{cl}">{t}</text>')
def line(points,color='#c9c4ad',w=15):
pts=' '.join(f'{xy(x,y)[0]},{xy(x,y)[1]}' for x,y in points); parts.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{w}" stroke-linejoin="round"/>')
line([(1,13),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],w=24)
line([(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2),(20,-4.6),(18.2,-5.7)],w=16)
rect(0,0,12,6,'#d8d0b7'); rect(0,4.2,12,2.4,'#e7deca')
rect(-14,-8.5,8,5,'#d8d0b7')
rect(-8.6,-1.8,5.2,1.6,'#c2ad89'); rect(-11.2,-3.9,1.6,5.8,'#c2ad89')
for y in [-1.38,1.30]:
for i in range(9): rect(-4.82+i*1.205,y,1.10,1.65,'#263e4b','#9aa9ac')
for y in [-10.3,-13]:
for i in range(6): rect(7.2+i*1.21,y,1.10,1.65,'#263e4b','#9aa9ac')
for cx,cy,w,d in [(11.3,-.6,7,5),(-25,-8.5,7,5),(0,-21,8,6)]:
rect(cx,cy,w,d,'none',css='future')
if cy!=-21:
for x in [cx-w/2+.3,cx+w/2-.3]:
for y in [cy-d/2+.3,cy+d/2-.3]: rect(x,y,.35,.35,'#a5aaa3')
for x,y in [(-18.95,-4),(-21.25,-4.2)]:
x1,y1=xy(x,y); parts.append(f'<circle cx="{x1}" cy="{y1}" r="{S}" fill="#345c44"/>')
for x in [17.8,21.2]:
for y in [-2.7,.6,3.9]: rect(x,y,2.35,1.25,'#718454','#a28862')
rect(18.2,-8.5,4.3,5,'#d4e4d9','#7b9b8c')
for x in [17,19.4]: rect(x,-8.5,.7,4.3,'#7a905c')
for x,y,r in [(18,-13.4,1.5),(24,-14.4,1.7),(25,-4.7,1.4)]:
xx,yy=xy(x,y); parts.append(f'<circle cx="{xx}" cy="{yy}" r="{r*S}" fill="#b8c69a" stroke="#90a076"/>')
text(0,4.0,'NORTH VERANDA · 2.4 m','small')
text(0,-4.5,'Living pavilion · 12 × 6 m')
text(0,-5.7,'18 roof PV modules','small')
text(-14,-8.4,'Sleeping')
text(-14,-9.6,'8 × 5 m','small')
text(-8.0,-.3,'Breezeway','small')
text(11.3,-.5,'Future office','small')
text(-25,-8.5,'Future guest','small')
text(0,-21,'Future workshop','small')
text(10.3,-15.5,'12 ground PV modules','small')
text(18.2,-12.5,'Greenhouse','small')
text(19.6,6,'Six raised food beds','small')
text(-20.6,-1.9,'Two rainwater tanks','small')
text(23,-17,'Mango + avocado','small')
parts.append('''<line x1="70" y1="915" x2="1330" y2="915" stroke="#c6cbbc"/><text x="70" y="953" class="note">Build now: two pavilions, breezeway, greenhouse, garden beds, tanks and solar.</text><text x="70" y="982" class="note">Dashed outlines: future additions. Concrete pier stubs at office and guest locations.</text><text x="70" y="1013" class="small">Concept layout only. Solar capacity, storage, shading and water yield require site-specific sizing. All dimensions in metres.</text></svg>''')
(out/'samford-solar-site-layout.svg').write_text(''.join(parts))

View File

@@ -0,0 +1,45 @@
import bpy,bmesh,math,random,os
from mathutils import Vector
random.seed(523)
OUT=os.path.dirname(os.path.abspath(__file__))
s=bpy.context.scene
# Ensure all closed construction solids and the atmosphere have outward-facing surfaces.
fixed=0
for o in bpy.data.objects:
if o.type=='MESH' and len(o.data.vertices)==8 and len(o.data.polygons)==6:
bm=bmesh.new(); bm.from_mesh(o.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(o.data); bm.free(); fixed+=1
print('SOLID_NORMALS_CHECKED',fixed,flush=True)
def ground(x,y): return -.024*y+.004*x+.12*math.sin(x*.19)*math.cos(y*.14)+.055*math.sin(x*.72+y*.32)
# Put both tanks on the north-west side of the sleeping module, visible from the main camera.
prefixes=('2000 mm dark green poly rainwater tank','Poly tank','Tank service lid','Accessible tank outlet','Gravel tank pad')
for o in bpy.data.objects:
if not o.name.startswith(prefixes): continue
center=o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
shift=Vector((0,4.1,0)) if center.y>-9 else Vector((-2.3,6.4,0))
shift.z=ground(center.x+shift.x,center.y+shift.y)-ground(center.x,center.y)
o.location+=shift
# Loose gravel fragments soften the hard geometry of unsealed paths.
vs=[]; fs=[]
routes=[([(1,14),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],1.5), ([(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2)],1.1), ([(20,2),(20,-4.6),(18.2,-5.7)],1)]
for points,width in routes:
for a,b in zip(points,points[1:]):
a,b=Vector(a),Vector(b); delta=b-a; side=Vector((-delta.y,delta.x)).normalized()
for _ in range(int(delta.length*135)):
p=a+delta*random.random()+side*random.uniform(-width*.62,width*.62); x,y=p; z=ground(x,y)+.04; r=random.uniform(.013,.04); k=len(vs)
vs.extend([(x-r,y-r,z),(x+r,y-r*.5,z),(x+r*.7,y+r,z),(x-r*.7,y+r*.7,z),(x,y,z+r*.8)])
fs.extend([(k,k+1,k+4),(k+1,k+2,k+4),(k+2,k+3,k+4),(k+3,k,k+4)])
me=bpy.data.meshes.new('Loose gravel stones'); me.from_pydata(vs,[],fs); me.materials.append(bpy.data.materials['Loose local gravel - fine stones']); ob=bpy.data.objects.new('Scattered loose gravel and irregular path edges',me); bpy.data.collections['07 Terrain and paths'].objects.link(ob)
# Modest golden light; a low-density atmosphere scatters it through the canopy.
s.world.node_tree.nodes['Background'].inputs['Strength'].default_value=.6
s.view_settings.exposure=.3
sun=bpy.data.objects['Warm early morning sun from north-east']; sun.data.energy=3.0
v=next(n for n in bpy.data.materials['Very light canopy haze'].node_tree.nodes if n.type=='PRINCIPLED_VOLUME'); v.inputs['Density'].default_value=.0012
for area in [a for screen in bpy.data.screens for a in screen.areas if a.type=='VIEW_3D']:
area.spaces.active.shading.type='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.region_3d.view_perspective='CAMERA'
bpy.data.objects['Subtle atmosphere over hillside'].hide_set(True)
# hide_set affects only the interactive viewport; the atmosphere remains enabled for render.
s.render.resolution_percentage=100; s.cycles.samples=96; s.render.filepath=os.path.join(OUT,'samford-modular-final.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
s.cycles.device='CPU'; s.cycles.samples=12; s.render.resolution_percentage=40; s.render.filepath=os.path.join(OUT,'samford-modular-light-check.png')
bpy.ops.render.render(write_still=True)
print('LIGHTING_CHECK_COMPLETE',flush=True)

View File

@@ -0,0 +1,65 @@
import bpy, random, math, os
from mathutils import Vector
random.seed(119)
OUT=os.path.dirname(os.path.abspath(__file__))
s=bpy.context.scene
# Layer individual leaves into substantial evergreen canopies instead of sparse twigs.
for ob in list(bpy.data.objects):
if 'individual canopy leaves' not in ob.name: continue
old=ob.data
verts=[tuple(v.co) for v in old.vertices]; faces=[tuple(p.vertices) for p in old.polygons]; inds=[p.material_index for p in old.polygons]
original=list(verts)
for k in range(0,len(original),5):
center=Vector(original[k+4]); index=inds[(k//5)*4]
for repeat in range(6):
offset=Vector((random.uniform(-.36,.36),random.uniform(-.36,.36),random.uniform(-.23,.23)))
a=random.uniform(-1,1); scale=random.uniform(.95,1.35); j=len(verts)
for v in original[k:k+5]:
d=(Vector(v)-center)*scale
d=Vector((d.x*math.cos(a)-d.y*math.sin(a),d.x*math.sin(a)+d.y*math.cos(a),d.z))
verts.append(tuple(center+offset+d))
faces.extend([(j,j+1,j+4),(j+1,j+2,j+4),(j+2,j+3,j+4),(j+3,j,j+4)]); inds.extend([index]*4)
me=bpy.data.meshes.new(ob.name+' full layered foliage'); me.from_pydata(verts,[],faces); me.update()
for m in old.materials: me.materials.append(m)
for p,mi in zip(me.polygons,inds): p.material_index=mi
ob.data=me; bpy.data.meshes.remove(old)
print('CANOPY',ob.name,len(verts),flush=True)
# Move two foreground eucalyptus trees aside to leave greenhouse and beds visible.
for ob in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
if not ob.data or ob.type!='MESH': continue
center=sum((Vector(b) for b in ob.bound_box),Vector())/8
center=ob.matrix_world@center
if center.x>26 and center.y>5: ob.location.x+=17
# Soft blue ambient sky with a warm north-east morning sun.
world=s.world; nodes=world.node_tree.nodes; bg=nodes.get('Background')
for link in list(bg.inputs['Color'].links): world.node_tree.links.remove(link)
bg.inputs['Color'].default_value=(.38,.49,.64,1); bg.inputs['Strength'].default_value=.45
s.view_settings.exposure=0
sun=bpy.data.objects['Warm early morning sun from north-east']; sun.data.energy=2.3; sun.data.color=(1,.78,.53)
sun.location=(35,40,23); sun.rotation_euler=(Vector((-15,-20,0))-sun.location).to_track_quat('-Z','Y').to_euler()
# Broad overlapping, wooded ridgelines complete the landscape beyond the site.
collection=bpy.data.collections['07 Terrain and paths']
for row,(y,base,color) in enumerate([(-58,9,(.13,.21,.16)),(-87,17,(.20,.29,.26)),(-125,23,(.29,.37,.36))]):
mat=bpy.data.materials.new('Distant wooded ridge '+str(row)); mat.diffuse_color=(*color,1); mat.use_nodes=True; p=mat.node_tree.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*color,1); p.inputs['Roughness'].default_value=1
vs=[]; fs=[]
for j in range(9):
yy=y+(j-4)*7
for i in range(81):
x=-180+i*4.5; crest=base+4*math.sin(x*.048+row)+2.2*math.sin(x*.105+row*2)
z=crest*max(0,1-((j-4)/4.5)**2)-2
vs.append((x,yy,z))
for j in range(8):
for i in range(80):
k=j*81+i; fs.append((k,k+1,k+82,k+81))
me=bpy.data.meshes.new('Ridgeline terrain'); me.from_pydata(vs,[],fs); me.materials.append(mat); ob=bpy.data.objects.new('Layered subtropical ridgeline',me); collection.objects.link(ob)
for p in me.polygons: p.use_smooth=True
# Tone down new timber and roof glare; retain warm structure and silver-grey cladding.
for name in ['Silvered hardwood weatherboard - X grain','Silvered hardwood weatherboard - Y grain']:
m=bpy.data.materials[name]; ramp=next(n for n in m.node_tree.nodes if n.type=='VALTORGB')
ramp.color_ramp.elements[0].color=(.115,.12,.105,1); ramp.color_ramp.elements[1].color=(.30,.30,.26,1)
bpy.data.objects['NE aerial - complete adaptable compound'].data.lens=48
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.render.filepath=os.path.join(OUT,'samford-modular-final.png'); s.cycles.samples=96
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
s.cycles.device='CPU'; s.cycles.samples=12; s.render.resolution_percentage=40; s.render.filepath=os.path.join(OUT,'samford-modular-refined-draft.png')
bpy.ops.render.render(write_still=True)
print('REFINED_DRAFT_COMPLETE',flush=True)

View File

@@ -0,0 +1,9 @@
import bpy,os
s=bpy.context.scene
s.camera=bpy.data.objects['Alternate - living pavilion and open veranda']
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
for d in prefs.devices: d.use=d.type=='METAL'
s.cycles.device='GPU'; s.cycles.samples=32; s.cycles.adaptive_threshold=.04; s.cycles.use_denoising=True
s.render.filepath=os.path.join(os.path.dirname(__file__),'samford-living-pavilion-detail.png')
bpy.ops.render.render(write_still=True)

View File

@@ -0,0 +1,38 @@
import bpy, os, json
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
OUT=os.path.dirname(os.path.abspath(__file__))
s=bpy.context.scene
if not s.get('Orchard canopy clearance applied'):
for ob in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
if ob.type!='MESH': continue
c=ob.matrix_world@(sum((Vector(v) for v in ob.bound_box),Vector())/8)
if 15<c.x<28 and 5<c.y<15:
ob.location.y-=23; ob.location.z+=.55
s['Orchard canopy clearance applied']=True
names=[o.name for o in s.objects]
counts={
'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),
'rainwater_tanks':sum(n.startswith('2000 mm dark green poly rainwater tank') for n in names),
'raised_garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),
'sliding_door_panels':sum(n.startswith('Sliding glass pane') and o.location.y>3 for n,o in zip(names,s.objects)),
'future_pier_stubs':sum('installed concrete pier stub' in n for n in names),
}
assert counts['solar_panels']==30,counts
assert counts['rainwater_tanks']==2,counts
assert counts['raised_garden_beds']==6,counts
assert counts['sliding_door_panels']==6,counts
assert counts['future_pier_stubs']==8,counts
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100
s.cycles.samples=96; s.cycles.adaptive_threshold=.025; s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type='METAL'; prefs.get_devices()
for d in prefs.devices: d.use=d.type=='METAL'
s.cycles.device='GPU' if any(d.type=='METAL' for d in prefs.devices) else 'CPU'
s.render.filepath=os.path.join(OUT,'samford-modular-final.png')
report={'render_engine':s.render.engine,'resolution':[2560,1440],'counts':counts,'objects':len(s.objects),'packed_external_textures_required':False}
with open(os.path.join(OUT,'scene-checks.json'),'w') as f: json.dump(report,f,indent=2)
print('SCENE_CHECKS',json.dumps(report),flush=True)
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
bpy.ops.render.render(write_still=True)
print('FINAL_RENDER_COMPLETE',flush=True)

View File

@@ -0,0 +1,16 @@
{
"render_engine": "CYCLES",
"resolution": [
2560,
1440
],
"counts": {
"solar_panels": 30,
"rainwater_tanks": 2,
"raised_garden_beds": 6,
"sliding_door_panels": 6,
"future_pier_stubs": 8
},
"objects": 2686,
"packed_external_textures_required": false
}

View File

@@ -0,0 +1,36 @@
# Samford solarpunk homestead — smooth tour
Open **samford-solarpunk-smooth-tour.blend**. It is saved at frame 1 in the animated camera view. Hover over the 3D view and press **Space**, or click the Timeline Play triangle. Space pauses. **Shift + Left Arrow** returns to the start. The tour runs for **115 seconds across 23 chapters**.
## Smoother playback and solarpunk update
The interactive scene uses simplified foliage and merged static display meshes: 47 visible objects and approximately 198,000 faces, down from 4,090 objects and 1.91 million faces. Matching Workbench preview tests were approximately 6–7 times faster after warm-up; these timings are not a measurement of interactive FPS. Solid shading is selected, with expensive cavity and shadow effects disabled.
Full editable geometry and PBR materials remain under **DETAIL - full geometry for renders**. This collection is disabled only in the viewport. Cycles renders automatically use the detailed scene and exclude **00 FAST TOUR - lightweight viewport**. For detailed editing, expose the monitor column in the Outliner filter menu, enable DETAIL and disable FAST TOUR; reverse those two switches for playback. The lightweight meshes are static display copies, so edit the originals for final renders.
The garage now contains a simplified pearl-white Tesla Model Y with panoramic glazing, Tesla badge, aero wheels and a cable connected to a wall charger. Eight additional garage roof solar modules and a planted timber trellis extend the solarpunk theme. The vehicle is a concept representation based on the [2020–2024 Tesla Model Y reference](https://www.tesla.com/ownersmanual/2020_2024_modely/en_cn/Owners_Manual.pdf). Solar charging is a visual concept, with no electrical capacity simulation.
`tesla-solarpunk-garage.png` shows the updated garage. `performance-checks.json` records performance and model verification.
## What changed
- Two of the three solid children’s cabins are removed, including their furniture and unnecessary covered walks. Their locations are now faint wire outlines and translucent ground shadows, labelled as unbuilt future spaces in the tour.
- The remaining cabin is a meditation room with a woven mat, square zabuton, one round cushion, a low stone candle shelf with three candles, and a simple timber bench. The former bed, desk, wardrobe and decoration are removed.
- A 4m-wide gravel driveway approaches from the southeast and reaches a gravel turning apron outside a detached 8 × 7m garage. The garage contains the Tesla, a timber workstation with tools and a small display, and two bicycles in a three-position rack.
- The living pavilion, parents’ wing, corridor, meditation room, sauna, greenhouse and garage have transparent presentation facades. Timber posts and frames remain visible.
- Kitchen/dining, parents’ bedroom and bathroom, six dining seats, homelab, solar, two rainwater tanks, greenhouse and garden beds remain.
## Transparent viewing sides
Collection **30 VIEW - Transparent building sides** contains the retained wall skins. They use near-transparent materials in Cycles and are hidden as filled surfaces in the fast solid viewport. These are visual cutaways, not a specification to build open or transparent exterior walls. Each wall object records its original material names for restoration. The rest of the building remains editable.
## Files
- `retreat-garage-overview.png` — road, garage and homestead.
- `meditation-room.png` — cushion, candles and simple interior.
- `garage-workstation-bikes.png` — parked car, workbench and bikes.
- `retreat-garage-plan.svg` — updated labelled layout.
- `design-checks.json` — scene counts and tour verification.
- `tour-chapters.json` — chapter frames and titles.
The earlier family-cabin version is preserved in the adjacent samford-spacious-tour folder. This is an architectural scene, not a surveyed site or construction design. Road access and gradients, drainage, garage structure, ventilation, fire and final equipment requirements still depend on the actual property and detailed design.

View File

@@ -0,0 +1,117 @@
# Samford Solarpunk Homestead — Unreal Engine 5 project scope
Prepared 10 September 2026. Proposal only; no Unreal installation, import or project build has been performed.
## Recommended outcome
An editable UE5 architectural scene and a local Mac application for exploring the current homestead. Keep Blender as the source for building design; use Unreal for real-time presentation. The first release preserves the current layout, natural material palette, solar infrastructure and future expansion footprints.
Assumptions: one local user, keyboard/mouse or trackpad, this Mac as the first target, fixed golden-hour lighting, and no construction-grade or energy simulation. The scope reuses the existing concept models; a commercial-quality replacement vehicle or furniture library is additional art work.
## Existing source and local readiness
- Source: `samford-solarpunk-smooth-tour.blend` in this directory. Its hidden DETAIL collection contains the complete render geometry. Its FAST TOUR collection is a separate, simplified viewport copy. Exporting both would duplicate the scene.
- Existing assets: living/kitchen/dining pavilion; parents' bedroom and bathroom wing; connecting corridor and verandahs; meditation room; two future shadow footprints; sauna and shower; greenhouse and six beds; tanks; roof and ground solar; Starlink and homelab; driveway; garage, Tesla concept car, charging point, workbench and bikes.
- Existing tour: 115 seconds, 23 chapters, animated camera and lens, chapter labels and deliberate cuts.
- Measured Blender display: approximately 4,090 visible objects / 1.91 million faces before optimization, versus 47 objects / 198,000 faces in the fast version. These are Blender polygon counts, not Unreal triangle counts or Unreal performance measurements.
- Local hardware: Apple M5, 24 GiB unified memory. macOS 26.6.2; Xcode 26.6. Approximately 194 GiB free on the workspace volume at inspection.
- No UnrealEditor application was found by Spotlight; no Epic Games installation was present in the standard shared folder or Applications listing. An installation elsewhere remains possible.
- Epic recommends 32 GB or more memory. The current Mac is above the listed minimum but below that recommendation. The pilot must establish a practical visual-quality target on this machine. [Epic Mac requirements](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
## Approaches considered
| Approach | Estimated effort | Result |
|---|---:|---|
| Basic conversion | 12–24 hours | Imported geometry, basic materials, simple fly view. Useful proof, limited polish and interaction. |
| Optimized interactive homestead — recommended | 48–80 hours total | Walk/fly controls, complete tour, cutaways, future footprints, rebuilt materials and vegetation, tested local Mac application. |
| Showcase visualization | 100–180 hours total | Recommended version plus substantial model upgrades, richer botanical assets, more elaborate cinematics and atmosphere. |
These are planning estimates for one experienced Unreal architectural-visualization generalist, not measured execution times or a fixed quote. Installation downloads and first shader compilation add elapsed time. The options are alternative totals, not additive phases.
## First release: user experience
1. A simple start screen: **Walk**, **Fly**, **Take the tour**, **Quality**, **Quit**.
2. Walk at human scale through the home, meditation cabin, garage, sauna and garden. Floors, stairs/ramps and door openings must work physically; presentation-only wall transparency is not sufficient.
3. Fly freely to understand the terrain, solar arrays, road and spatial relationships.
4. Play the 23-chapter tour with pause/resume, restart and chapter selection. Recreate captions as screen UI rather than 3D camera-attached text.
5. In fly/exploration view, **See inside** toggles the presentation wall sections. Walk mode uses coherent physical boundaries and accessible doors.
6. **Future spaces** shows/hides the two existing ghost footprints and their labels. Fully furnished future buildings are outside this first release.
7. **Balanced** and **Fast** presets adjust foliage, lighting, shadows and rendering resolution. Keep the interface small and nontechnical.
The Tesla, solar arrays, server, tanks and sauna are visual assets. Driving, charging simulation, water balance, food production and heat simulation are not included.
## Conversion and scene design
### Asset preparation
Work from a duplicate of the detailed source. Create stable export names and pivots; apply necessary modifiers; check normals, openings, thin surfaces and object scale. Validate that Blender metres become the correct Unreal centimetres and that camera orientation, sun direction and site north remain consistent.
Use FBX mesh exports grouped by building and asset family, with an explicit placement manifest for repeatable imports. Run a small glTF comparison only if FBX material or hierarchy import is troublesome. Keep doors, removable presentation walls, roofs, floors and furniture as logical separate pieces. Merge minor static parts locally and instance repeated posts, panels and vegetation. Avoid importing thousands of unrelated tiny Actors, or merging the entire property into one mesh.
FBX scene import supports geometry and basic materials, but it does not guarantee the source appearance or animated-camera import. This is a conversion workflow, not a one-click format change. [Epic FBX scene import](https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-scene-import-in-unreal-engine)
### Materials and lighting
Create UVs where needed. Bake or rebuild the Blender procedural materials: timber grain, clay, concrete/stone, bamboo, metals, solar glass, glazing, vegetation and upholstery. Start with reusable 1K–2K texture sets and use higher resolution only where the close-up view requires it. Check texture colour spaces, normal-map orientation and transparency.
Rebuild the warm morning lighting and restrained haze in Unreal. Test software Lumen first, with a simpler fixed-lighting alternative if necessary. Epic currently lists software Lumen on Apple Silicon, hardware-ray-traced Lumen/MegaLights as unsupported on Mac, and Nanite/Virtual Shadow Maps on M2+ as beta support. Nanite will not be a baseline dependency. [Epic rendering feature support](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
### Terrain and vegetation
Retain the existing terrain shape for the first release; introduce a native Unreal Landscape only if sculpting or foliage workflow materially benefits. Convert trees, groundcover and garden plants into reusable meshes with distance-based detail and culling. Preserve the open views and subtropical character. Use instanced static-mesh foliage; large amounts of individual Actor foliage would repeat the current scene's object-overhead problem. [Epic foliage workflow](https://dev.epicgames.com/documentation/en-us/unreal-engine/foliage-mode-in-unreal-engine)
### Tour, interaction and updates
Build one Blueprint-based local experience and a Sequencer tour. Transfer sampled camera transforms and lens values, then verify axes, cuts, interpolation and all chapter boundaries. FBX camera animation needs a dedicated Sequencer workflow or a sampled-data importer; it should not be assumed to arrive with the static scene. [Epic cinematic animation import](https://dev.epicgames.com/documentation/unreal-engine/import-and-export-cinematic-fbx-animations-in-unreal-engine)
Use separate groups for present buildings, future outlines, cutaway faces, landscape and services. Keep interaction logic separate from imported geometry so subsequent Blender reimports do not erase it. A full live synchronization system is not included.
## Work packages and effort
| Stage | Work and checkpoint | Hours |
|---|---|---:|
| 1. Compatibility and pilot | Confirm engine/toolchain; import one furnished room, glass, one tree and a short camera move; package and benchmark on this Mac. Choose rendering approach. | 4–8 |
| 2. Geometry conversion | Clean and export the whole site; organize assets, pivots and instances; repair openings and build simple collision. | 8–12 |
| 3. Materials | Bake/rebuild the material palette; validate indoor and outdoor appearance and texture memory. | 10–16 |
| 4. Landscape and lighting | Optimize vegetation, terrain, shadows, warm lighting and haze. | 10–16 |
| 5. Interaction and tour | Walking/flying, doors as needed, 23 chapters, simple menu, future and cutaway toggles. | 10–16 |
| 6. Performance and delivery | Profile packaged build, tune quality presets, test the entire tour and deliver editable project and local app. | 6–12 |
| **Total** | **Approximately 6–10 eight-hour working days** | **48–80** |
The pilot is included in the total. If it cannot meet a usable baseline on this Mac, resolve the lighting/foliage approach before converting the remaining assets. Installation/toolchain problems or substantial remodeling can move the estimate outside this range.
## Acceptance criteria
- Local Mac application launches without the Unreal Editor open; editable project reopens successfully.
- All listed current structures and contents are present at the correct scale, with no duplicate FAST/DETAIL export.
- All intended rooms and outdoor paths are accessible in walk mode; no falling through floors or invisible blocking presentation walls.
- All 23 chapters play in order, pause/resume/restart work, and each chapter shows its intended subject without unintended clipping.
- Future and cutaway toggles work; reopening the application returns to a sensible starting state.
- No missing textures or materially incorrect glass, black surfaces or severe light leaks on the agreed inspection route.
- Initial performance target: 30 FPS at 1920×1080 output in Balanced mode, using upscaling if needed. Aim for 60 FPS in Fast mode. Both are targets, not promises before the pilot.
- Benchmark the packaged application after shader warm-up over a repeatable exterior/interior route; record average and slow-frame timings, quality settings and memory behavior. Complete a ten-minute session without crashes or sustained memory pressure. Profile editor performance separately.
## Deliverables
- Editable Unreal project with organized assets and an asset/source map.
- Local Mac application, tested on this machine.
- Preserved Blender original and a documented export/reimport workflow.
- 23-chapter tour, room navigation and the agreed toggles.
- Performance results and a short illustrated controls guide.
- Optional recorded walkthrough video; final video resolution and quality can be set independently of real-time playback.
## Budget, dependencies and exclusions
Engine fees may be zero under Epic's individual/small-business terms. Commercial use must be checked against the applicable revenue and licence rules. Optional licensed plants/material assets: allow A$0–500 initially; no purchases are assumed. [Epic licensing](https://www.unrealengine.com/license)
For comparison only, hiring 48–80 hours at an assumed A$100–160/hour implies approximately A$4,800–12,800 in labour before GST where applicable. The hourly rates are budgeting assumptions, not researched vendor quotes. This is separate from the cost of constructing the real house and from any Codex subscription or usage charges.
Allow approximately 100–150 GB of working storage for the chosen engine, project, imports and caches, subject to installer options and measured cache growth. Avoid a source-engine build or large unneeded content libraries for this first release.
The installed Xcode is 26.6. Epic currently lists 26.1.1 as recommended for UE 5.8 and specifically excludes 26.4; this does not establish whether 26.6 works. Verify a compatible engine/toolchain pair in the pilot. Do not globally replace the user's developer-tool configuration as a default setup step.
Additional scope: Windows packaging/testing; public distribution signing/notarization; VR; browser streaming and hosting; multiplayer; a driveable Tesla; realistic weather/time-of-day controls; fully built future dwellings; a material configurator; photogrammetry/site survey alignment; substantial remodels; architectural compliance; energy, water or construction-cost simulation. The current terrain is a concept landscape, not a surveyed Samford property.
## Recommended next milestone
A 4–8-hour compatibility and visual pilot using the living pavilion, a tree, key materials and one camera shot. Its output is a runnable Mac sample, measured frame times, confirmed toolchain and a recommendation for the final visual settings. That makes the full conversion estimate concrete before committing to the remaining work.

View File

@@ -0,0 +1,218 @@
import bpy,math,os,json
from mathutils import Vector,Matrix
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene
detail=bpy.data.collections['DETAIL - full geometry for renders']
col=bpy.data.collections.new('33 Daily life - training gathering play and food');detail.children.link(col)
COL={'life':col};active='life'
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];roof=bpy.data.materials['Pale Colorbond - powder coated steel'];linen=bpy.data.materials['Undyed flax linen'];gravel=bpy.data.materials['Loose local gravel - fine stones'];F=1.12
rubber=mat('Exercise mat muted charcoal',(.035,.048,.044),.94)
sage=mat('Table tennis muted green',(.10,.21,.16),.63)
cream=mat('Ceramic tea bowls warm white',(.73,.68,.56),.48)
white=mat('Table tennis painted court lines',(.8,.79,.71),.63)
black=mat('Freezer seals and equipment grips',(.028,.03,.029),.7)
metal=mat('Appliance brushed metal',(.44,.46,.43),.36,.7)
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
def path(n,pts,w):
vs=[];fs=[]
for a,b in zip(pts,pts[1:]):
a,b=Vector(a),Vector(b);delta=b-a;perp=Vector((-delta.y,delta.x)).normalized()*w/2;steps=max(2,int(delta.length/.3));off=len(vs)
for i in range(steps+1):
q=a+delta*i/steps
for v in [q-perp,q+perp]:vs.append((v.x,v.y,ground(v.x,v.y)+.085))
for i in range(steps):k=off+2*i;fs.append((k,k+1,k+3,k+2))
return mesh(n,vs,fs,gravel)
def deck(n,cx,cy,w,d,z):
box(n+' accessible pier deck',(cx,cy,z-.09),(w,d,.18),wood,.01)
for x in [cx-w/2+.15,cx+w/2-.15]:
for y in [cy-d/2+.15,cy+d/2-.15]:
g=ground(x,y);box(n+' stone pier',(x,y,(g+z-.18)/2),(.3,.3,max(.08,z-.18-g)),stone,.01)
for i in range(int(d/.15)):
box(n+' deck board joint',(cx,cy-d/2+.075+i*.15,z+.002),(w,.006,.003),steel)
def bench(n,x,y,z,length=2.7):
box(n+' seat',(x,y,z+.45),(length,.43,.065),wood,.015)
box(n+' linen seat pad',(x,y,z+.502),(length-.1,.39,.04),linen,.016)
for xx in [x-length*.37,x+length*.37]:box(n+' leg',(xx,y,z+.21),(.07,.35,.42),woodz,.008)
box(n+' backrest',(x,y+.21,z+.75),(length,.055,.27),wood,.012)
def ring(n,c,r,t,material):
cx,cy,cz=c;vs=[];fs=[];N=48;M=10
for i in range(N):
a=i*2*pi/N
for j in range(M):
b=j*2*pi/M;rr=r+t*cos(b);vs.append((cx+rr*cos(a),cy+t*sin(b),cz+rr*sin(a)))
for i in range(N):
for j in range(M):fs.append((i*M+j,((i+1)%N)*M+j,((i+1)%N)*M+(j+1)%M,i*M+(j+1)%M))
ob=mesh(n,vs,fs,material)
for p in ob.data.polygons:p.use_smooth=True
return ob
# Covered exercise pavilion west of the garage. Independent steel exercise frame.
tx,ty,tz=-12.2,-18,.95
deck('TRAINING',tx,ty,4.8,5.4,tz)
for x in [tx-2.28,tx+2.28]:
for y,h in [(ty-2.57,2.95),(ty+2.57,3.25)]:
box('Training timber canopy post',(x,y,tz+h/2),(.125,.125,h),woodz,.005)
box('Training canopy steel shoe',(x,y,tz+.07),(.16,.16,.14),steel,.005)
for x in [tx-2.28+i*.57 for i in range(9)]:beam('Training exposed roof rafter',(x,ty-2.9,tz+2.95),(x,ty+2.9,tz+3.29),.065,.18,wood)
ro=box('Training pale mono pitch canopy',(tx,ty,tz+3.16),(5.3,6,.065),roof,.006);ro.rotation_euler.x=math.atan(.3/5.4)
for x in [tx-.98,tx+.98]:
box('EXERCISE FRAME - steel upright',(x,ty+.85,tz+1.4),(.095,.095,2.8),steel,.006)
box('Exercise frame bolted foot plate',(x,ty+.85,tz+.02),(.30,.36,.04),steel,.005)
for dx in [-.1,.1]:
for dy in [-.12,.12]:rod('Exercise frame anchor concept',(x+dx,ty+.85+dy,tz+.04),(x+dx,ty+.85+dy,tz+.067),.011,metal,sides=8)
bar=rod('PULL UP BAR - dedicated steel frame',(tx-1.02,ty+.85,tz+2.70),(tx+1.02,ty+.85,tz+2.70),.017,steel,sides=20)
bar['Design intent']='Dedicated exercise frame intended to be load rated by its manufacturer or structural engineer. These concept anchor plates do not establish a safe working load.'
for x in [tx-.28,tx+.28]:
box('Adjustable ring hanging strap',(x,ty+.85,tz+2.10),(.027,.009,1.18),black,.002)
ring('WOODEN GYMNASTIC RING',(x,ty+.85,tz+1.40),.125,.017,wood)
for x,y in [(tx-.65,ty-.5),(tx+.55,ty-.5)]:box('MOVABLE EXERCISE MAT',(x,y,tz+.022),(.85,1.85,.045),rubber,.025)
box('Adjustable weight bench seat',(tx+1.55,ty-.88,tz+.45),(.40,.38,.09),rubber,.035)
back=box('ADJUSTABLE BENCH - inclined back',(tx+1.55,ty-.25,tz+.60),(.40,.91,.09),rubber,.035);back.rotation_euler.x=.3
beam('Bench adjustable steel spine',(tx+1.55,ty-1.05,tz+.15),(tx+1.55,ty+.23,tz+.41),.065,.065,steel)
for y in [ty-1,ty+.15]:box('Bench stabilising foot',(tx+1.55,y,tz+.065),(.59,.065,.065),steel,.009)
box('Dumbbell timber stand',(tx-1.55,ty-.5,tz+.72),(.68,1.25,.06),wood,.012)
for y in [ty-.95,ty-.05]:
box('Dumbbell stand leg',(tx-1.55,y,tz+.34),(.5,.06,.68),steel)
for y in [ty-.80,ty-.22]:
rod('ADJUSTABLE DUMBBELL grip',(tx-1.73,y,tz+.85),(tx-1.37,y,tz+.85),.019,steel,sides=16)
for x in [tx-1.77,tx-1.33]:
for dx in [-.06,-.02,.02,.06]:box('Dumbbell selectable weight plate',(x+dx,y,tz+.85),(.027,.22,.23),black,.008)
for i,x in enumerate([-9.60,-9.25,-8.90]):
top=.76-.19*i;box('Training access step',(x,-17,top-.08),(.36,1.2,.16),wood,.009)
path('Training to garage pedestrian link',[(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],1.1)
# Outdoor dining: a separate terrace keeps the original narrow veranda clear.
dx,dy=10,3.5;deck('GATHERING TERRACE',dx,dy,6,5.8,F)
deck('Veranda terrace connection',6.5,4.2,1,1.5,F)
box('OUTDOOR TABLE - ten place timber top',(dx,dy,F+.76),(3.2,1.03,.085),wood,.025)
for x in [dx-1.13,dx+1.13]:box('Outdoor table trestle',(x,dy,F+.36),(.10,.78,.72),woodz,.01)
bench('Outdoor north bench - four places',dx,dy+.98,F,2.95)
south_start=set(col.objects);bench('Outdoor south bench - four places',dx,dy-.98,F,2.95)
for o in set(col.objects)-south_start:
if 'backrest' in o.name:o.location.y=dy-1.19
for x in [dx-1.94,dx+1.94]:
box('Outdoor end chair seat',(x,dy,F+.45),(.47,.47,.06),wood,.012)
for a in [-.17,.17]:
for b in [-.17,.17]:box('Outdoor end chair leg',(x+a,dy+b,F+.21),(.038,.038,.42),woodz)
box('Outdoor end chair back',(x+(-.21 if x<dx else .21),dy,F+.75),(.055,.47,.36),wood,.01)
for x in [7.15,12.85]:
for y in [.75,6.25]:box('Gathering pergola post',(x,y,F+1.38),(.12,.12,2.76),woodz,.006)
for y in [.75,6.25]:box('Gathering pergola beam',(10,y,F+2.76),(6,.12,.20),wood,.005)
for x in [7.1+i*.35 for i in range(18)]:box('Gathering pergola shade slat',(x,3.5,F+2.88),(.065,5.8,.14),wood)
# Nearby movable tea counter and shared bowls.
box('TEA AND MATCHA - timber preparation cart',(11.6,5.74,F+.88),(1.55,.58,.06),wood,.016)
for x in [11,12.2]:
for y in [5.55,5.93]:box('Tea cart frame',(x,y,F+.43),(.045,.045,.86),woodz)
box('Tea cart storage shelf',(11.6,5.74,F+.25),(1.46,.50,.04),wood)
rod('Tea kettle',(11.15,5.72,F+.92),(11.15,5.72,F+1.13),.10,metal,sides=24)
for x in [11.5,11.76,12.02]:rod('Matcha ceramic bowl',(x,5.73,F+.92),(x,5.73,F+1.01),.075,cream,r2=.095,sides=24)
for x in [8.9,9.7,10.5,11.3]:
for y in [dy-.28,dy+.28]:rod('Shared table cup',(x,y,F+.81),(x,y,F+.89),.037,cream,sides=16)
# Table tennis has its own level deck, clear of vehicles; folding halves remain editable.
px,py,pz=11,-8.5,.80;deck('PLAY DECK',px,py,5.5,6.5,pz)
for y in [py-.695,py+.695]:
box('FOLDING TABLE TENNIS - hinged half',(px,y,pz+.76),(1.525,1.37,.045),sage,.004)
for x in [px-.50,px+.50]:
box('Table tennis folding leg',(x,y,pz+.38),(.035,.035,.71),steel,.004)
rod('Table tennis caster',(x-.025,y,pz+.035),(x+.025,y,pz+.035),.034,black,sides=12)
for x in [px-.746,px+.746,px]:box('Table tennis white longitudinal line',(x,py,pz+.784),(.012,2.73,.002),white)
for y in [py-1.36,py+1.36]:box('Table tennis white end line',(px,y,pz+.785),(1.52,.018,.002),white)
for x in [px-.82,px+.82]:rod('Table tennis net post',(x,py,pz+.76),(x,py,pz+.915),.007,steel,sides=8)
for i in range(7):rod('Table tennis horizontal net cord',(px-.82,py,pz+.77+i*.024),(px+.82,py,pz+.77+i*.024),.0015,white,sides=5)
for i in range(43):rod('Table tennis vertical net cord',(px-.8+i*.038,py,pz+.77),(px-.8+i*.038,py,pz+.914),.0012,white,sides=5)
for i,x in enumerate([8.05,7.70,7.35]):
top=.62-.18*i;box('Play deck access step',(x,-7,top-.08),(.36,1.2,.16),wood,.009)
path('Play deck pedestrian connection',[(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],1.1)
# A reserved footprint only: no court surface or working hoop is built now.
future=bpy.data.materials['Future shadow - faint sage outline']
for a,b in [((8,-41),(18,-41)),((18,-41),(18,-31)),((18,-31),(8,-31)),((8,-31),(8,-41))]:
a,b=Vector(a),Vector(b)
for i in range(10):
p=a+(b-a)*i/10;q=a+(b-a)*(i+.55)/10
beam('FUTURE BASKETBALL - dashed reserve',(p.x,p.y,ground(p.x,p.y)+.09),(q.x,q.y,ground(q.x,q.y)+.09),.07,.04,future)
# Walking loop: joins the quiet room and garden without crossing building reserves.
loop=[(-11,14.7),(-14,18),(-11,23),(1,24),(13,23),(20,19),(19,10),(30,10),(33,3),(33,-10),(27,-13),(20,-11),(17,-4),(14,1),(14,10),(19,10)]
path('SHADED WALKING LOOP - garden and quiet room',loop,1.05)
path('Walking loop return to home',[(14,1),(13,4),(13,7),(6,7)],1.05)
path('Walking loop home to meditation',[(-1,24),(-7,21),(-14,18)],1.05)
bench('QUIET GARDEN BENCH',30,10.9,ground(30,10.9)+.07,1.8)
for x in [29,31]:box('Quiet bench shade post',(x,11,ground(x,11)+1.2),(.10,.10,2.4),woodz,.006)
for x in [28.9+i*.26 for i in range(9)]:box('Quiet bench timber shade slat',(x,11,ground(30,11)+2.45),(.06,1.6,.10),wood)
# Everyday food: clear prep counter, pantry and compact freezer along the south wall.
box('BREAKFAST - clear preparation counter',(.22,-2.52,F+.94),(1.15,.73,.055),stone,.013)
box('Breakfast counter timber base',(.22,-2.55,F+.45),(1.10,.62,.89),wood,.009)
box('Breakfast chopping board',(.30,-2.48,F+.982),(.42,.30,.025),wood,.008)
for x in [-.19,.02]:rod('Breakfast cereal jar',(x,-2.73,F+.98),(x,-2.73,F+1.18),.066,cream,sides=20)
box('PANTRY - tall timber cabinet',(1.32,-2.55,F+1.06),(.85,.65,2.12),wood,.014)
for x in [1.12,1.52]:box('Pantry door pull',(x,-2.21,F+1.05),(.023,.028,.23),steel,.004)
box('FREEZER - compact upright',(2.27,-2.55,F+.66),(.72,.69,1.32),cream,.025)
box('Freezer insulated door',(2.27,-2.19,F+.68),(.68,.05,1.23),metal,.014)
box('Freezer door handle',(2.52,-2.14,F+.89),(.025,.04,.32),steel,.007)
for x in [2.04,2.5]:box('Freezer ventilation clearance foot',(x,-2.54,F+.035),(.065,.48,.07),black)
hx,hy=29.8,1.5;hz=ground(hx,hy)+.08
box('HARVEST BENCH - garden sorting surface',(hx,hy,hz+.90),(.78,2.05,.065),wood,.018)
for x in [hx-.28,hx+.28]:
for y in [hy-.81,hy+.81]:box('Harvest bench leg',(x,y,hz+.43),(.06,.06,.86),woodz,.006)
box('Harvest bench lower storage',(hx,hy,hz+.22),(.70,1.95,.045),wood)
for y in [hy-.6,hy+.10]:
box('Harvest timber produce crate',(hx,y,hz+1.045),(.57,.48,.24),wood,.012)
box('Harvest crate dark interior',(hx,y,hz+1.171),(.49,.4,.01),bpy.data.materials['Damp brown garden soil'])
layout={'training':[-12.2,-18,4.8,5.4],'outdoor_table':[10,3.5,6,5.8],'table_tennis':[11,-8.5,5.5,6.5],'future_basketball':[13,-36,10,10],'walking_loop':loop,'harvest_bench':[29.8,1.5], 'outdoor_seats':10}
json.dump(layout,open(OUT+'/daily-life-layout.json','w'),indent=2)
exec(compile(open(OUT+'/enhance_kitchen.py').read(),OUT+'/enhance_kitchen.py','exec'))
# A single merged viewport copy for new geometry.
verts=[];faces=[];mi=[];mats=[];mapping={}
for o in col.objects:
if o.type!='MESH':continue
remap=[]
for m in o.data.materials:
if m not in mapping:mapping[m]=len(mats);mats.append(m)
remap.append(mapping[m])
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
verts.extend(tuple(world@v.co) for v in o.data.vertices)
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mi.append(remap[p.material_index])
me=bpy.data.meshes.new('Daily life lightweight display');me.from_pydata(verts,[],faces);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',mi)
proxy=bpy.data.objects.new('FAST | Training gathering play and garden loop',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
cam=bpy.data.objects['PLAY TOUR camera']
shots=[('25 / TRAINING & STRENGTH',(-8,-12,4),(-8.8,-12.8,3.8),(-12.2,-18,2),25),('26 / OUTDOOR TABLE & MATCHA',(15,9,4.8),(14,8,4.4),(10,3.5,1.6),28),('27 / PLAY & FUTURE BASKETBALL',(17,-1,5),(16,-2,4.7),(11,-8.5,1),29),('28 / WALKING & FOOD GARDEN',(35,16,8),(33,14,7),(27,3,1.3),30),('29 / EASY EVERYDAY FOOD',(-.3,.4,2.9),(.1,.1,2.8),(1,-2.5,2),24)]
caption_col=bpy.data.collections['22 PLAY - guided camera tour']
ref=max((o for o in caption_col.objects if o.type=='FONT'),key=lambda o:o.name)
ref.scale=(0,0,0);ref.keyframe_insert('scale',frame=2881)
shots.append(('30 / COPPER & MARBLE KITCHEN',(-.8,2.5,3.1),(-1.0,2.2,2.95),(-3.45,-1.2,1.9),25))
chapters=json.load(open(OUT+'/tour-chapters-reading.json'))
for idx,(title,a,b,target,lens) in enumerate(shots):
start=2881+idx*120;end=start+119
for f,p in [(start,a),(end,b)]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=f);cam.keyframe_insert('rotation_euler',frame=f);cam.data.lens=lens;cam.data.keyframe_insert('lens',frame=f)
caption=ref.copy();caption.data=ref.data.copy();caption.animation_data_clear();caption.data.body=title;caption.name='Tour chapter '+title;caption_col.objects.link(caption)
half=36/lens;caption.location=(-half*.91,half*9/16*.84,-2)
for f,sz in [(1,0),(start-1,0),(start,1),(end,1),(end+1,0)]:
caption.scale=(sz,sz,sz);caption.keyframe_insert('scale',frame=f)
s.timeline_markers.new(title,frame=start);chapters.append({'title':title,'start':start,'end':end,'seconds':5})
for title in caption_col.objects:
if title.type=='FONT' and title.animation_data:
for layer in title.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for fc in bag.fcurves:
if fc.data_path=='scale':
for k in fc.keyframe_points:k.interpolation='CONSTANT'
s.frame_end=3600;s.frame_set(3481);s.camera=cam
json.dump(chapters,open(OUT+'/tour-chapters-daily-life.json','w'),indent=2)
s['Daily life additions']='Covered exercise area, ten-place outdoor table and tea counter, folding table tennis, unbuilt basketball reserve, garden walking loop and quiet bench, breakfast counter, pantry/freezer and harvest bench.'
s['Training design status']='Concept geometry only: dedicated exercise frame requires appropriate manufacturer rating/engineering and equipment clearances; future basketball rectangle is a reserved area, not a built level court.'
report={'training_area':1,'pullup_bar':1,'wooden_rings':2,'adjustable_dumbbells':2,'adjustable_bench':1,'exercise_mats':2,'outdoor_seats':10,'tea_counter':1,'folding_table_tennis':1,'basketball_status':'Future reserve only','walking_loop':True,'quiet_bench':1,'breakfast_counter':1,'pantry':1,'freezer':1,'harvest_bench':1,'tour_chapters':30,'tour_seconds':150,'kitchen':kitchen_checks}
json.dump(report,open(OUT+'/daily-life-checks.json','w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-daily-life-tour.blend')
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT':o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=24;s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU'
for fname,frame in [('training-area.png',2920),('outdoor-gathering.png',3035),('kitchen-copper-marble.png',3520)]:
s.frame_set(frame);s.render.filepath=OUT+'/'+fname;bpy.ops.render.render(write_still=True)
s.frame_set(3300);cam.animation_data_clear();cam.data.animation_data_clear();cam.location=(48,55,48);cam.rotation_euler=(Vector((2,-8,0))-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.lens=37;s.render.filepath=OUT+'/daily-life-overview.png';bpy.ops.render.render(write_still=True)
print('DAILY_LIFE_READY '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,102 @@
import bpy,math,os,random,json,bmesh
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene;random.seed(901)
col=bpy.data.collections.new('31 Reading library - books and lounge chair');bpy.data.collections['DETAIL - full geometry for renders'].children.link(col)
COL={'reading':col};active='reading'
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes']
fabric=textured('Reading chair moss linen',(.16,.21,.12),(.34,.39,.25),(95,95,95),.92,.003)
paper=mat('Books warm uncoated paper',(.74,.69,.54),.95)
palette=[(.24,.31,.24),(.16,.25,.29),(.46,.22,.13),(.56,.41,.21),(.34,.26,.20),(.69,.61,.44),(.25,.17,.18),(.11,.16,.16),(.42,.45,.33),(.57,.35,.24)]
covers=[mat('Cloth book cover %02d'%i,c,.88) for i,c in enumerate(palette)]
ink=mat('Book spine gold print',(.56,.42,.23),.68)
F=1.12;sx,sy=5.67,-.05
# Two bays in one substantial wall bookcase, opening west into the main room.
box('LIBRARY - solid timber bookshelf back',(sx+.18,sy,F+1.10),(.028,2.65,2.20),wood,.005)
for y in [sy-1.32,sy,sy+1.32]:box('Library upright',(sx,y,F+1.10),(.39,.042,2.20),woodz,.009)
levels=[.12,.51,.90,1.29,1.68,2.10]
for z in levels:box('Library full length timber shelf',(sx,sy,F+z),(.41,2.69,.045),wood,.01)
box('Library recessed plinth',(sx+.03,sy,F+.045),(.30,2.52,.09),woodz,.01)
book_count=0
for bay in range(2):
start=sy-1.28+bay*1.32;end=start+1.23
for row in range(5):
y=start+.035
while y<end-.07:
w=random.uniform(.033,.078)
if y+w>end:break
h=random.uniform(.24,.325);d=random.uniform(.205,.285);z=F+levels[row]+.023;material=random.choice(covers);x=sx-.18+d/2+.025
book_count+=1;n='Library book %03d'%book_count
box(n+' paper block',(x,y+w/2,z+h/2),(d-.008,w-.008,h-.014),paper,.001)
for yy in [y+.002,y+w-.002]:box(n+' cloth cover',(x,yy,z+h/2),(d+.012,.004,h),material,.0015)
box(n+' visible spine',(x-d/2-.004,y+w/2,z+h/2),(.010,w,h),material,.002)
for hz in [.055,h-.04]:box(n+' spine band',(x-d/2-.0095,y+w/2,z+hz),(.001,w*.7,.004),ink)
y+=w+random.uniform(.008,.020)
# A low, generously proportioned timber-and-linen reading chair faces the valley.
before=set(col.objects);chairx,chairy=4.77,1.78
box('READING CHAIR - timber seat frame',(0,0,F+.34),(.78,.79,.08),wood,.035)
box('Reading chair deep linen seat',(0,.025,F+.46),(.70,.72,.18),fabric,.09)
back=box('Reading chair reclined linen back',(0,-.33,F+.84),(.70,.17,.65),fabric,.085);back.rotation_euler.x=math.radians(-12)
for x in [-.38,.38]:
box('Reading chair timber arm',(x,-.01,F+.68),(.067,.84,.065),wood,.025)
for y in [-.29,.29]:beam('Reading chair square timber leg',(x,y,F+.04),(x,y-.055,F+.65),.045,.045,woodz)
beam('Reading chair back support',(x,-.29,F+.34),(x,-.46,F+1.12),.045,.045,woodz)
angle=math.radians(48)
from mathutils import Matrix
rot=Matrix.Rotation(angle,4,'Z');trans=Matrix.Translation((chairx,chairy,0))
for o in set(col.objects)-before:
local=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
o.matrix_world=trans@rot@local
box('Reading nook side table top',(3.78,2.03,F+.54),(.48,.44,.055),wood,.02)
for x in [3.60,3.96]:
for y in [1.87,2.19]:box('Reading side table leg',(x,y,F+.26),(.035,.035,.52),woodz,.005)
box('Reading table closed book',(3.78,2.02,F+.586),(.26,.18,.035),covers[2],.004)
rod('Reading floor lamp base',(5.40,2.37,F),(5.40,2.37,F+.035),.15,steel,sides=32)
rod('Reading floor lamp stem',(5.40,2.37,F+.035),(5.40,2.37,F+1.43),.012,steel,sides=16)
lampmat=mat('Reading lamp undyed linen shade',(.68,.59,.42),.9)
rod('Reading floor lamp linen shade',(5.40,2.37,F+1.32),(5.40,2.37,F+1.61),.22,lampmat,r2=.135,sides=48)
ld=bpy.data.lights.new('Reading nook warm lamp','POINT');ld.energy=55;ld.color=(1,.69,.38);ld.shadow_soft_size=.18
lo=bpy.data.objects.new(ld.name,ld);col.objects.link(lo);lo.location=(5.4,2.37,F+1.34)
# One lightweight static display mesh keeps the existing fast tour responsive.
verts=[];faces=[];mis=[];mats=[];index={}
for o in col.objects:
if o.type!='MESH':continue
remap=[]
for m in o.data.materials:
if m not in index:index[m]=len(mats);mats.append(m)
remap.append(index[m])
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
off=len(verts);verts.extend(tuple(world@v.co) for v in o.data.vertices)
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mis.append(remap[p.material_index])
me=bpy.data.meshes.new('Reading corner lightweight display');me.from_pydata(verts,[],faces);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',mis)
o=bpy.data.objects.new('FAST | Reading library and chair',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(o)
# Append a five-second final chapter showing the new reading corner.
cam=bpy.data.objects['PLAY TOUR camera']
for frame,loc,target in [(2761,(1.2,4.65,3.5),(4.7,.5,1.9)),(2880,(1.65,4.3,3.35),(4.7,.5,1.9))]:
s.frame_set(frame);cam.location=loc;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=frame);cam.keyframe_insert('rotation_euler',frame=frame);cam.data.lens=29;cam.data.keyframe_insert('lens',frame=frame)
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
original=titles[-1];title=original.copy();title.data=original.data.copy();title.animation_data_clear();title.name='Tour chapter 24 - reading library';title.data.body='24 / BOOKS & A QUIET READING CORNER';bpy.data.collections['22 PLAY - guided camera tour'].objects.link(title)
for f,scale in [(1,0),(2760,0),(2761,1),(2880,1)]:title.scale=(scale,)*3;title.keyframe_insert('scale',frame=f)
if title.animation_data:
action=title.animation_data.action
for layer in action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for fc in bag.fcurves:
for k in fc.keyframe_points:k.interpolation='CONSTANT'
s.timeline_markers.new('24 / BOOKS & READING',frame=2761);s.frame_end=2880;s.frame_set(2761);s.camera=cam
s['Reading library']='Two-bay timber bookshelf filled with %d books; linen-and-timber reading chair, side table and reading lamp in the main living room.'%book_count
report={'books':book_count,'bookshelf_bays':2,'reading_chairs':1,'tour_chapters':24,'tour_seconds':120,'fast_proxy_objects_added':1}
json.dump(report,open(OUT+'/reading-corner-checks.json','w'),indent=2)
chapters=json.load(open(OUT+'/tour-chapters.json'));chapters[20]['title']='21 / TESLA, SOLAR CHARGING & WORKSHOP';chapters.append({'title':'24 / BOOKS & A QUIET READING CORNER','start':2761,'end':2880,'seconds':5});json.dump(chapters,open(OUT+'/tour-chapters-reading.json','w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-reading-tour.blend')
for o in titles+[title]:o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=24;s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU';s.render.filepath=OUT+'/reading-corner.png';bpy.ops.render.render(write_still=True)
print('READING_CORNER_DONE '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,11 @@
{
"eight_sleep_cover": 1,
"eight_sleep_hub": 1,
"blackout_curtain_panels": 2,
"curtain_closed_shape_keys": 2,
"indoor_ac": 1,
"outdoor_condenser": 1,
"tour_chapters": 24,
"tour_seconds": 120,
"new_fast_display_meshes": 1
}

View File

@@ -0,0 +1,105 @@
import bpy, math, os, json
from mathutils import Vector, Matrix
from math import sin, cos, pi
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
detail=bpy.data.collections['DETAIL - full geometry for renders']
col=bpy.data.collections.new('32 Bedroom - Eight Sleep blackout and air conditioning');detail.children.link(col)
COL={'sleep':col};active='sleep'
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes']
linen=bpy.data.materials['Undyed flax linen'];stone=bpy.data.materials['Split sandstone facing - mineral grain']
black=textured('Eight Sleep charcoal cover textile',(.027,.029,.033),(.065,.067,.071),(110,110,110),.91,.001)
hubmat=mat('Eight Sleep hub graphite casing',(.042,.045,.049),.48)
white=mat('Quiet AC warm white casing',(.79,.78,.73),.38)
lining=mat('Opaque blackout curtain lining',(.095,.09,.08),1)
curtainmat=textured('Blackout curtain oatmeal woven face',(.27,.24,.18),(.48,.43,.34),(100,100,20),.98,.002)
F=1.12;bx=-12.5;by=-5
cover=box('EIGHT SLEEP - fitted temperature regulating cover',(bx,by,1.727),(1.91,2.16,.065),black,.035)
cover['Product']='Eight Sleep Pod cover and bedside Hub; approximate concept geometry, not a manufacturer CAD model.'
box('Eight Sleep fitted cover north skirt',(bx,-3.914,1.65),(1.89,.022,.15),black,.018)
for x in [-13.454,-11.546]:box('Eight Sleep fitted cover side skirt',(x,by,1.65),(.025,2.10,.15),black,.014)
box('Bed folded undyed linen throw',(bx,-4.24,1.79),(1.83,.59,.07),linen,.025)
hx,hy=-13.97,-5.60
box('EIGHT SLEEP - bedside cooling Hub',(hx,hy,F+.245),(.29,.41,.47),hubmat,.045)
box('Eight Sleep hub top lid',(hx,hy,F+.485),(.255,.365,.014),black,.028)
box('Eight Sleep hub front intake',(hx,hy+.207,F+.24),(.22,.008,.32),black,.006)
for z in range(18):box('Hub ventilation slot',(hx,hy+.213,F+.09+z*.016),(.205,.004,.004),steel,.001)
for x in [hx-.10,hx+.10]:
for y in [hy-.14,hy+.14]:box('Hub rubber isolating foot',(x,y,F+.014),(.033,.04,.028),black,.004)
# Twin hoses stay near the bed head and clear of the circulation route.
for dx in [-.02,.02]:
points=[(hx+dx,hy-.19,F+.19),(hx+dx,-6.21,F+.14),(-13.40+dx,-6.21,F+.16),(-13.40+dx,-6.04,1.68)]
for a,b in zip(points,points[1:]):rod('Eight Sleep water hose',a,b,.012,black,sides=12)
labeldata=bpy.data.curves.new('Eight Sleep wordmark','FONT');labeldata.body='EIGHT SLEEP';labeldata.size=.032;labeldata.align_x='CENTER';labeldata.materials.append(white)
label=bpy.data.objects.new('Eight Sleep Hub label',labeldata);col.objects.link(label);label.location=(hx,hy+.218,F+.405);label.rotation_euler=(pi/2,0,pi)
# Pleated curtains: two real opaque layers; shape keys expose the closed position.
box('Blackout curtain ceiling track',(-12.65,-2.69,4.22),(5.45,.06,.055),steel,.009)
box('Blackout curtain timber pelmet',(-12.65,-2.73,4.245),(5.5,.23,.20),wood,.01)
curtains=[]
for side,edge,sign in [('left',-15.35,1),('right',-9.95,-1)]:
vs=[];fs=[];steps=64
for z in [1.14,4.18]:
for i in range(steps+1):
u=i/steps;vs.append((edge+sign*.82*u,-2.68+.065*cos(u*2*pi*8),z))
for i in range(steps):fs.append((i,i+1,steps+i+2,steps+i+1))
o=mesh('BLACKOUT CURTAIN - '+side,vs,fs,curtainmat);o.data.materials.append(lining)
o.shape_key_add(name='Basis - open for viewing');closed=o.shape_key_add(name='Closed - blackout')
for i,v in enumerate(closed.data):v.co.x=edge+sign*2.73*(i%(steps+1))/steps
closed.value=0.0;o.active_shape_key_index=0
m=o.modifiers.new('Opaque blackout lining','SOLIDIFY');m.thickness=.007;m.material_offset=1
o['How to close']='Object Data Properties > Shape Keys > Closed - blackout: set Value to 1 on both panels. DETAIL collection must be enabled for editing.'
curtains.append(o)
# Indoor split head on the west wall near the foot of the bed.
ax,ay,az=-15.57,-3.48,3.67
box('AIR CONDITIONER - bedroom indoor split head',(ax,ay,az),(.23,.92,.30),white,.055)
box('Air conditioner dark outlet',(ax+.118,ay,az-.09),(.014,.79,.065),black,.008)
for z in [az-.11,az-.075]:box('Air conditioner adjustable louvre',(ax+.13,ay,z),(.07,.78,.012),white,.003)
box('Air conditioner status panel',(ax+.12,ay-.29,az+.045),(.009,.10,.035),hubmat,.003)
box('AC remote on bedroom wall',(-15.67,-4.18,2.33),(.038,.065,.15),white,.008)
box('AC remote display',(-15.647,-4.18,2.35),(.009,.048,.045),hubmat,.002)
# External condenser on isolation feet, behind the sleeping pavilion.
cx,cy,cz=-14.70,-8.12,.79
box('AC outdoor unit stable pad',(cx,cy,.39),(1.03,.67,.10),stone,.015)
box('AIR CONDITIONER - outdoor condenser',(cx,cy,cz),(.88,.39,.64),white,.035)
for x in [cx-.30,cx+.30]:box('AC outdoor vibration mount',(x,cy,.46),(.085,.32,.04),black,.005)
rod('AC outdoor circular fan grille',(cx-.12,cy-.202,cz),(cx-.12,cy-.215,cz),.24,black,sides=48)
for i in range(11):
x=cx-.34+i*.044
half=math.sqrt(max(0,.235**2-(x-(cx-.12))**2))
rod('Condenser grille bar',(x,cy-.22,cz-half),(x,cy-.22,cz+half),.0045,steel,sides=6)
for z in range(9):box('Condenser service ventilation',(cx+.30,cy-.20,cz-.20+z*.048),(.14,.008,.011),steel,.002)
pipepoints=[(-15.94,-3.48,3.65),(-15.94,-7.72,3.65),(-15.94,-7.72,.8),(-15.05,-7.72,.8),(-15.05,-7.92,.8)]
for a,b in zip(pipepoints,pipepoints[1:]):beam('AC accessible insulated service trunking',a,b,.075,.065,white)
rod('AC condensate drain down exterior',(-15.94,-3.5,3.52),(-15.94,-3.5,.5),.013,white,sides=10)
s['Bedroom sleep setup']='Eight Sleep Pod-style fitted cover with ventilated bedside Hub, lined blackout curtains with closed shape keys, indoor split AC and outdoor condenser. Concept placement; final product clearances and HVAC sizing are unresolved.'
# Preserve fast tour performance with one display proxy for the new static pieces.
verts=[];faces=[];indices=[];materials=[];mapping={}
for o in col.objects:
if o.type!='MESH':continue
remap=[]
for m in o.data.materials:
if m not in mapping:mapping[m]=len(materials);materials.append(m)
remap.append(mapping[m])
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
verts.extend(tuple(world@v.co) for v in o.data.vertices)
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));indices.append(remap[p.material_index])
me=bpy.data.meshes.new('Bedroom sleep equipment lightweight display');me.from_pydata(verts,[],faces);me.update()
for m in materials:me.materials.append(m)
me.polygons.foreach_set('material_index',indices)
proxy=bpy.data.objects.new('FAST | Bedroom sleep equipment',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
# Refine the existing bedroom chapter without changing the two-minute tour length.
cam=bpy.data.objects['PLAY TOUR camera']
for frame,loc,target in [(841,(-10.70,-6.95,3.30),(-13.10,-4.60,1.97)),(960,(-10.95,-6.73,3.20),(-13.10,-4.52,1.97))]:
s.frame_set(frame);cam.location=loc;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=frame);cam.keyframe_insert('rotation_euler',frame=frame);cam.data.lens=20;cam.data.keyframe_insert('lens',frame=frame)
s.frame_set(841);s.camera=cam
report={'eight_sleep_cover':1,'eight_sleep_hub':1,'blackout_curtain_panels':2,'curtain_closed_shape_keys':2,'indoor_ac':1,'outdoor_condenser':1,'tour_chapters':24,'tour_seconds':120,'new_fast_display_meshes':1}
json.dump(report,open(OUT+'/bedroom-sleep-checks.json','w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-bedroom-tour.blend')
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT':o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=32;s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU';s.render.filepath=OUT+'/bedroom-sleep-setup.png';bpy.ops.render.render(write_still=True)
print('BEDROOM_SLEEP_READY '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,54 @@
from pathlib import Path
import json,html
O=Path(__file__).parent;d=json.loads((O/'garden-flow-layout.json').read_text());p=[]
p.append('<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1180" viewBox="0 0 1400 1180"><rect width="1400" height="1180" fill="#f3f0e7"/><style>text{font-family:Arial,sans-serif;fill:#30433b}.sub{fill:#68756b}</style><text x="60" y="58" font-size="31" font-weight="600">A home held together by its garden.</text><text x="60" y="91" font-size="16" class="sub">SAMFORD VALLEY / Aligned pavilions, soft garden curves and room to grow · Concept 004</text>')
def pt(x,y):return (600+x*13,500-y*13)
def line(pts,color,w,dash=''):
q=' '.join(f'{pt(x,y)[0]:.1f},{pt(x,y)[1]:.1f}' for x,y in pts);p.append(f'<polyline points="{q}" fill="none" stroke="{color}" stroke-width="{w}" stroke-linejoin="round" stroke-linecap="round" {dash}/>')
def rect(cx,cy,w,h,fill='#d9c5a5',dash=''):
x,y=pt(cx-w/2,cy+h/2);p.append(f'<rect x="{x}" y="{y}" width="{w*13}" height="{h*13}" fill="{fill}" stroke="#657666" stroke-width="1.5" {dash}/>')
def label(x,y,lines,size=11):
X,Y=pt(x,y)
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{html.escape(t)}</text>')
def ellipse(cx,cy,rx,ry,fill):
x,y=pt(cx,cy);p.append(f'<ellipse cx="{x}" cy="{y}" rx="{rx*13}" ry="{ry*13}" fill="{fill}"/>')
line([(0,-23),(0,28)],'#c1cbbc',1,'stroke-dasharray="4 6"')
ellipse(0,13.2,7.8,4.6,'#c7d4b5')
for path in d['paths']:
color='#c9bda6' if 'VEHICLE' in path['name'] else ('#b59b75' if path['kind']=='roofed walk' else '#d9d2bf')
line(path['points'],color,path['width']*13)
rect(2,-12.5,12,6,'#c9bda6')
for bed in d['beds']:
x,y=bed['center'];rx,ry=bed['radii'];ellipse(x,y,rx,ry,'#8fa981')
for xx in [-.5,0,.5]:ellipse(x+xx*rx,y,.12,.12,'#d5b6b5')
rect(*d['main']);rect(0,4.2,12,2.4,'#c5cfb6');label(0,4.4,['VERANDA'],11)
rect(-3.8,.1,2.2,.95,'#f5f2e9');label(1,1,['MAIN HOUSE','Kitchen · dining','Living / reading'],10)
rect(*d['parents']);label(-11.8,-4.4,['PARENTS','Bedroom + bath'],11)
rect(-7.75,-1.8,3.5,1.2,'#c8bba5')
rect(*d['meditation']);label(-11.8,12.5,['MEDITATION','ROOM'],10)
for idx,shape in enumerate(d['future']):
rect(*shape,'#e7ecdf','stroke-dasharray="5 5"');label(shape[0],shape[1]+.6,['FUTURE',f'PAVILION {idx+1}'],10)
label(0,13.5,['SHARED','GARDEN COURT'],11)
label(0,28,['ONE CENTRAL AXIS'],11)
label(-16.7,18.6,['Curved covered','timber breezeway'],11)
line([(-16.7,17),(-17,8.5),(-8.3,7.2)],'#8b9785',1)
rect(*d['outdoor_table'],'#d9c49f');rect(10,3.5,3.2,1.03,'#a48663');label(10,9,['TABLE FOR TEN','Tea + matcha'],10)
rect(*d['garage'],'#c7c2b3');rect(-1.7,-19.1,1.9,4.35,'#9da99c');label(1.5,-18.5,['GARAGE','Workshop','Bikes'],10)
rect(*d['training'],'#c1ceb1');label(-9.4,-17.9,['TRAINING','Bar + rings','Weights + mats'],10)
rect(*d['solar'],'#849a9d');label(9.4,-18.7,['GROUND SOLAR'],9)
label(0,-25,['Aligned garage, training and energy zone'],11)
x,y=d['sauna'];ellipse(x,y,2.2,2.0,'#ba9b75');label(x,y+.15,['SAUNA'],10)
rect(-27,6.8,1.3,1.3,'#b0c5c1');label(-29,10,['Outdoor shower'],10)
for x in [-23.95,-26.25]:ellipse(x,-8,1,1,'#6d8563')
label(-25,-10.8,['Rainwater tanks'],10)
rect(24.2,-7.5,4.3,5,'#c3d7c2');label(24.2,-7.4,['GREENHOUSE'],9)
for x in [23.8,27.5]:
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#ad9573')
label(25.5,8.5,['FOOD GARDEN'],11)
rect(29.8,1.5,.78,2.05,'#a78e6b');label(33.5,1.5,['Harvest bench'],10)
rect(30,10.9,1.8,.45,'#a78e6b');label(31.5,16,['Quiet bench'],10)
label(12,-7,['FLOWER','GARDEN'],10)
rect(*d['future_basketball'],'#edf0e6','stroke-dasharray="6 6"');label(13,-35,['FUTURE','BASKETBALL','Unbuilt reserve'],11)
label(27,-29,['4 m gravel access'],11)
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#30433b" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="65" y1="1030" x2="195" y2="1030" stroke="#30433b" stroke-width="4"/><text x="65" y="1060" font-size="13">10 metres</text>','<line x1="60" y1="1090" x2="1340" y2="1090" stroke="#c5cebd"/><text x="60" y="1120" font-size="14">13 planted garden beds · Smooth pedestrian routes · Future pavilions remain shadows · Table-tennis deck removed</text>','<text x="60" y="1147" font-size="13" class="sub">Concept only: illustrative terrain, no surveyed boundary. Plant forms and structural connections require site-specific design development.</text></svg>'])
(O/'garden-flow-plan.svg').write_text(''.join(p))

View File

@@ -0,0 +1,64 @@
from pathlib import Path
from html import escape
import json
O=Path(__file__).parent
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1120" viewBox="0 0 1400 1120"><rect width="1400" height="1120" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}</style><text x="60" y="60" font-size="32" font-weight="600">A home for movement, connection and quiet.</text><text x="60" y="95" font-size="17">SAMFORD VALLEY / Updated concept plan · 10 September 2026 · Version 003</text>']
def pt(x,y):return (630+13*x,470-13*y)
def label(x,y,lines,size=12):
X,Y=pt(x,y)
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
def rect(cx,cy,w,d,fill,stroke='#526154',dash=''):
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*13}" height="{d*13}" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
def line(points,color,width):
pts=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
layout=json.loads((O/'daily-life-layout.json').read_text())
line(layout['walking_loop'],'#adc49a',14)
line([(14,1),(13,4),(13,7),(6,7)],'#adc49a',14)
line([(-1,24),(-7,21),(-14,18)],'#adc49a',14)
label(9,27.4,['SHADED WALKING LOOP'],12)
line([(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)],'#c9bca1',52)
rect(-1,-12.5,12,6,'#c9bca1',stroke='#c9bca1')
line([(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],'#d5cdbb',12)
line([(-3,5.4),(-3,9),(-11,9)],'#b5a88c',16)
rect(0,0,12,6,'#ddc8a9');label(1,.7,['SHARED HOME','Kitchen · dining'],10)
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['VERANDA'],11)
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.8,1.5,['Short link'],10)
rect(-11.8,-4.9,8,5,'#ddc8a9');label(-11.8,-4.5,['PARENTS','Bedroom + bath'],11)
rect(-11,12,4.5,4.8,'#c9c1a5');label(-11,12.4,['QUIET','ROOM'],11)
X,Y=pt(-11,12);p.append(f'<circle cx="{X}" cy="{Y+24}" r="5" fill="#626257"/>')
for i,(x,y) in enumerate([(-3,17),(7,15)]):
rect(x,y,4.5,4.8,'#e6e8de',stroke='#98a394',dash='stroke-dasharray="5 5"');label(x,y+.6,['FUTURE',f'SPACE {i+1}'],11)
label(2,21.4,['Faint outlines only / build when needed'],12)
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="29" ry="25" fill="#bc9d75" stroke="#526154" stroke-width="2"/>');label(-23,2,['SAUNA'],10)
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7.5,['Outdoor shower'],11)
for x in [-23.95,-26.25]:
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="13" fill="#6b7f61"/>')
label(-25,-10.5,['Rainwater tanks'],11)
rect(-3,-19,8,7,'#bcb9ac');label(-3,-18.6,['GARAGE','8 × 7 m'],12)
rect(-4.7,-19.1,1.9,4.35,'#788c83');rect(-.47,-21.5,2.5,.76,'#ab9673')
label(-3,-24,['One car · workbench · bike racks'],12)
label(20,-25,['4 m gravel access road'],13)
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7.4,['GREENHOUSE'],9)
for x in [23.8,27.5]:
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
label(25.5,7.5,['FOOD GARDEN'],12)
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17.2,['GROUND SOLAR'],10)
label(34,-42,['Site approach'],12)
rect(*layout['training'],'#b9c8ad');label(-12.2,-16.8,['COVERED','TRAINING','4.8 × 5.4 m'],10)
line([(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],'#d5cdbb',12)
label(-15.8,-23.2,['Bar · rings · weights','Bench · movable mats'],11)
rect(*layout['outdoor_table'],'#d9c19b');rect(10,3.5,3.2,1.03,'#ab8962')
rect(10,4.48,2.95,.43,'#bda078');rect(10,2.52,2.95,.43,'#bda078')
rect(6.5,4.2,1,1.5,'#c8d0b8');label(10,9,['OUTDOOR TABLE','10 places + tea counter'],11)
rect(*layout['table_tennis'],'#ced8c4');rect(11,-8.5,1.525,2.74,'#608570')
label(11,-5.9,['TABLE TENNIS'],10);label(11,-12.8,['Folding table / level deck'],10)
line([(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],'#d5cdbb',12)
rect(*layout['future_basketball'],'#eceee4',stroke='#81947c',dash='stroke-dasharray="7 7"')
label(13,-35,['FUTURE','BASKETBALL','10 × 10 m reserve'],11)
label(13,-43,['Unbuilt / separate from driveway'],11)
rect(30,10.9,1.8,.45,'#a58a66');label(32,14,['QUIET BENCH'],11)
rect(29.8,1.5,.78,2.05,'#a58a66');label(35,2.4,['HARVEST','BENCH'],11)
label(4,-4.3,['Breakfast · pantry · freezer'],10)
rect(-3.8,.1,2.2,.95,'#f4f1e9')
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="70" y1="970" x2="200" y2="970" stroke="#263a34" stroke-width="4"/><text x="70" y="999" font-size="13">10 metres</text>','<line x1="60" y1="1040" x2="1340" y2="1040" stroke="#bdc3b6"/><text x="60" y="1070" font-size="15">The Blender presentation fades one facade of each building so you can see inside. Walls remain in the model.</text>','<text x="60" y="1096" font-size="13">Concept layout only. The road approach is illustrative; no surveyed property boundary or approved access is implied.</text></svg>'])
(O/'retreat-garage-plan.svg').write_text(''.join(p))

View File

@@ -0,0 +1,34 @@
{
"training_area": 1,
"pullup_bar": 1,
"wooden_rings": 2,
"adjustable_dumbbells": 2,
"adjustable_bench": 1,
"exercise_mats": 2,
"outdoor_seats": 10,
"tea_counter": 1,
"folding_table_tennis": 1,
"basketball_status": "Future reserve only",
"walking_loop": true,
"quiet_bench": 1,
"breakfast_counter": 1,
"pantry": 1,
"freezer": 1,
"harvest_bench": 1,
"tour_chapters": 30,
"tour_seconds": 150,
"kitchen": {
"oven": 1,
"induction_zones": 4,
"copper_stockpot": 1,
"copper_saucepan": 1,
"copper_frying_pan": 1,
"marble_island_m": [
2.2,
0.95
],
"island_rear_work_aisle_m": 1.67,
"old_simplified_kitchen_removed": true,
"fast_furniture_refreshed": true
}
}

View File

@@ -0,0 +1,97 @@
{
"training": [
-12.2,
-18,
4.8,
5.4
],
"outdoor_table": [
10,
3.5,
6,
5.8
],
"table_tennis": [
11,
-8.5,
5.5,
6.5
],
"future_basketball": [
13,
-36,
10,
10
],
"walking_loop": [
[
-11,
14.7
],
[
-14,
18
],
[
-11,
23
],
[
1,
24
],
[
13,
23
],
[
20,
19
],
[
19,
10
],
[
30,
10
],
[
33,
3
],
[
33,
-10
],
[
27,
-13
],
[
20,
-11
],
[
17,
-4
],
[
14,
1
],
[
14,
10
],
[
19,
10
]
],
"harvest_bench": [
29.8,
1.5
],
"outdoor_seats": 10
}

View File

@@ -0,0 +1,198 @@
{
"chapters": 30,
"end_frame": 3600,
"fps": 24,
"missing": [],
"caption_issues": [],
"caption_samples": [
{
"frame": 31,
"titles": [
"01 / THE SPACIOUS HOMESTEAD"
]
},
{
"frame": 151,
"titles": [
"02 / LIVING PAVILION & VERANDA"
]
},
{
"frame": 271,
"titles": [
"03 / KITCHEN & EXPOSED STRUCTURE"
]
},
{
"frame": 391,
"titles": [
"04 / DINING & OPEN LIVING"
]
},
{
"frame": 511,
"titles": [
"05 / ONE SERVER - LOCAL AI"
]
},
{
"frame": 631,
"titles": [
"06 / SHORT CORRIDOR TO PRIVATE ROOMS"
]
},
{
"frame": 751,
"titles": [
"07 / ADJACENT BEDROOM WING"
]
},
{
"frame": 871,
"titles": [
"08 / ENCLOSED BEDROOM"
]
},
{
"frame": 991,
"titles": [
"09 / BATHROOM - SEPARATE LOBBY DOOR"
]
},
{
"frame": 1111,
"titles": [
"10 / FINNISH SAUNA & OUTDOOR SHOWER"
]
},
{
"frame": 1231,
"titles": [
"11 / SAUNA BENCHES & STONE HEATER"
]
},
{
"frame": 1351,
"titles": [
"12 / OUTDOOR SHOWER"
]
},
{
"frame": 1471,
"titles": [
"13 / TWO RAINWATER TANKS"
]
},
{
"frame": 1591,
"titles": [
"14 / ROOFTOP SOLAR & STARLINK"
]
},
{
"frame": 1711,
"titles": [
"15 / SOLAR ARRAY & ENERGY STORAGE"
]
},
{
"frame": 1831,
"titles": [
"16 / FOOD GARDEN & GREENHOUSE"
]
},
{
"frame": 1951,
"titles": [
"17 / INSIDE THE GREENHOUSE"
]
},
{
"frame": 2071,
"titles": [
"18 / QUIET MEDITATION ROOM"
]
},
{
"frame": 2191,
"titles": [
"19 / CUSHION, CANDLES & STILLNESS"
]
},
{
"frame": 2311,
"titles": [
"20 / GRAVEL ROAD & GARAGE"
]
},
{
"frame": 2431,
"titles": [
"21 / TESLA, SOLAR CHARGING & WORKSHOP"
]
},
{
"frame": 2551,
"titles": [
"22 / FUTURE SPACES - BUILD LATER"
]
},
{
"frame": 2671,
"titles": [
"23 / A HOME THAT CAN CHANGE"
]
},
{
"frame": 2791,
"titles": [
"24 / BOOKS & A QUIET READING CORNER"
]
},
{
"frame": 2911,
"titles": [
"25 / TRAINING & STRENGTH"
]
},
{
"frame": 3031,
"titles": [
"26 / OUTDOOR TABLE & MATCHA"
]
},
{
"frame": 3151,
"titles": [
"27 / PLAY & FUTURE BASKETBALL"
]
},
{
"frame": 3271,
"titles": [
"28 / WALKING & FOOD GARDEN"
]
},
{
"frame": 3391,
"titles": [
"29 / EASY EVERYDAY FOOD"
]
},
{
"frame": 3511,
"titles": [
"30 / COPPER & MARBLE KITCHEN"
]
}
],
"render_engine": "CYCLES",
"render_size": [
2560,
1440
],
"detail_render_enabled": true,
"fast_excluded_from_render": true,
"training_steps": 3,
"play_steps": 3
}

View File

@@ -0,0 +1,30 @@
{
"future_shadow_spaces": 2,
"meditation_cushions": 1,
"candles": 3,
"garage_size_metres": [
8,
7
],
"cars": 1,
"bicycles": 2,
"rack_positions": 3,
"rainwater_tanks": 2,
"solar_panels": 30,
"garden_beds": 6,
"tour_seconds": 115,
"chapters": 23,
"transparent_wall_objects": 64,
"grass_faces_cleared_from_vehicle_surfaces": 6239,
"retained_child_beds": 0,
"transparent_buildings": [
"Living pavilion",
"Parents bedroom wing",
"Short corridor",
"Meditation room",
"Sauna",
"Greenhouse",
"Garage"
],
"all_23_chapters_animate": true
}

View File

@@ -0,0 +1,127 @@
# Executed by add_daily_life.py inside the scene's construction context.
# Replace the old simplified kitchen, retaining its position and the shared room.
old_prefixes=('Simple timber kitchen cabinet','Flat cabinet pull','Continuous kitchen worktop','Stainless sink','Exposed kitchen tap','Tap spout','Induction cooktop','Open timber kitchen shelf','Pantry ceramic jar')
for o in list(bpy.data.objects):
if o.name.startswith(old_prefixes):bpy.data.objects.remove(o,do_unlink=True)
# Pale marble: distorted fine mineral veins, soft honed reflections.
marble=mat('Honed natural marble - ivory and warm grey veins',(.78,.76,.70),.26)
ns=marble.node_tree.nodes;ls=marble.node_tree.links;bs=ns.get('Principled BSDF')
coord=ns.new('ShaderNodeTexCoord');noise=ns.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=2.8;noise.inputs['Detail'].default_value=5
ls.new(coord.outputs['Generated'],noise.inputs['Vector'])
scale=ns.new('ShaderNodeVectorMath');scale.operation='SCALE';scale.inputs[3].default_value=.33;ls.new(noise.outputs['Color'],scale.inputs[0])
add=ns.new('ShaderNodeVectorMath');add.operation='ADD';ls.new(coord.outputs['Generated'],add.inputs[0]);ls.new(scale.outputs[0],add.inputs[1])
wave=ns.new('ShaderNodeTexWave');wave.wave_type='BANDS';wave.bands_direction='DIAGONAL';wave.inputs['Scale'].default_value=3.7;wave.inputs['Distortion'].default_value=6;wave.inputs['Detail'].default_value=4;ls.new(add.outputs[0],wave.inputs['Vector'])
ramp=ns.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].position=.055;ramp.color_ramp.elements[0].color=(.20,.21,.20,1);ramp.color_ramp.elements[1].position=.14;ramp.color_ramp.elements[1].color=(.81,.79,.73,1)
ls.new(wave.outputs['Color'],ramp.inputs[0]);ls.new(ramp.outputs[0],bs.inputs['Base Color'])
fine=ns.new('ShaderNodeTexNoise');fine.inputs['Scale'].default_value=170;ls.new(coord.outputs['Generated'],fine.inputs['Vector']);bump=ns.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.10;bump.inputs['Distance'].default_value=.0005;ls.new(fine.outputs['Fac'],bump.inputs['Height']);ls.new(bump.outputs[0],bs.inputs['Normal'])
copperpot=textured('Copper cookware - brushed warm patina',(.32,.10,.035),(.72,.34,.13),(75,75,8),.26,.0006);copperpot.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.94
stainless=mat('Cookware food-contact stainless lining',(.55,.59,.61),.23,.96)
ovenblack=mat('Oven and induction reflective smoked glass',(.009,.016,.019),.12,.25)
mark=mat('Induction subtle etched markings',(.36,.42,.42),.45)
display=mat('Appliance soft white display',(.72,.82,.75),.3);display.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.65,.85,.74,1);display.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=.6
# Run of detailed cabinets, including a 600 mm oven bay.
for i,(x,w) in enumerate([(-5.15,.72),(-4.32,.90),(-3.36,.96),(-2.38,.96),(-1.49,.78),(-.88,.42)]):
box('Kitchen timber cabinet carcass',(x,-2.53,F+.45),(w,.64,.88),wood,.008)
box('Kitchen recessed dark plinth',(x,-2.53,F+.075),(w-.06,.55,.13),steel,.006)
if i==4:continue
for row,h in enumerate([.29,.56,.79]):
box('Kitchen flush timber drawer',(x,-2.194,F+h),(w-.018,.032,.22 if row<2 else .19),wood,.006)
rod('Kitchen brushed drawer pull',(x-.14,-2.162,F+h+.065),(x+.14,-2.162,F+h+.065),.009,metal,sides=12)
# Worktop parts leave a real sink opening at x=-4.32.
for x,w in [(-5.15,.80),(-2.405,3.21)]:box('Kitchen honed marble rear worktop',(x,-2.52,F+.925),(w,.75,.045),marble,.008)
for y,d in [(-2.80,.19),(-2.225,.16)]:box('Sink cutout marble edge',(-4.32,y,F+.925),(.86,d,.045),marble,.005)
box('Kitchen marble low splashback',(-3.1,-2.905,F+1.09),(4.95,.025,.28),marble,.006)
# Recessed sink basin with separate bottom and four walls.
box('Sink recessed basin floor',(-4.32,-2.5,F+.73),(.66,.40,.025),stainless,.015)
for x in [-4.66,-3.98]:box('Sink basin side',(x,-2.5,F+.825),(.025,.42,.20),stainless,.009)
for y in [-2.715,-2.285]:box('Sink basin end',(-4.32,y,F+.825),(.70,.025,.20),stainless,.009)
rod('Sink drain',(-4.32,-2.5,F+.744),(-4.32,-2.5,F+.750),.044,steel,sides=32)
# Curved copper tap, exposed and replaceable.
pts=[(-4.32,-2.8,F+.95),(-4.32,-2.8,F+1.26)]
for i in range(17):
a=pi-pi*i/16;pts.append((-4.32,-2.69+.11*cos(a),F+1.26+.11*sin(a)))
for i,(a,b) in enumerate(zip(pts,pts[1:])):rod('Copper mixer tap '+str(i),a,b,.014,copperpot,sides=16)
rod('Tap outlet',pts[-1],(-4.32,-2.58,F+1.20),.014,copperpot,sides=16)
rod('Tap lever',(-4.20,-2.78,F+.95),(-4.20,-2.78,F+1.07),.01,stainless,sides=12)
# Built-in oven, facing the living room (north).
ox=-1.49
box('OVEN - brushed steel surround',(ox,-2.177,F+.49),(.62,.04,.68),metal,.013)
box('OVEN - insulated glass door',(ox,-2.144,F+.43),(.555,.032,.44),ovenblack,.015)
box('Oven inner viewing panel',(ox,-2.125,F+.435),(.435,.006,.285),mat('Oven glass subtle interior',(.025,.035,.035),.18),.018)
for z in [F+.37,F+.49]:box('Oven internal rack hint',(ox,-2.120,z),(.39,.004,.008),metal)
rod('Oven tubular handle',(ox-.23,-2.085,F+.663),(ox+.23,-2.085,F+.663),.018,metal,sides=20)
for x in [ox-.22,ox+.22]:rod('Oven handle standoff',(x,-2.14,F+.663),(x,-2.085,F+.663),.012,metal,sides=16)
box('Oven control fascia',(ox,-2.142,F+.779),(.57,.028,.115),ovenblack,.007)
for x in [ox-.205,ox+.205]:rod('Oven rotary dial',(x,-2.13,F+.78),(x,-2.097,F+.78),.027,metal,sides=32)
box('Oven display',(ox,-2.121,F+.78),(.14,.004,.036),display,.002)
for i in range(9):box('Oven bottom ventilation slot',(ox-.24+i*.06,-2.15,F+.17),(.035,.009,.011),black,.002)
# Four-zone induction glass and etched circles.
box('INDUCTION - four zone glass cooktop',(-1.49,-2.51,F+.961),(.70,.53,.018),ovenblack,.009)
def horizontal_ring(name,x,y,z,r,t,material):
ob=ring(name,(0,0,0),r,t,material);ob.rotation_euler.x=pi/2;ob.location=(x,y,z);return ob
for x,y,r in [(-1.67,-2.64,.105),(-1.30,-2.64,.086),(-1.68,-2.40,.082),(-1.32,-2.40,.103)]:
horizontal_ring('Induction etched cooking zone',x,y,F+.973,r,.0012,mark)
for i in range(7):box('Induction touch slider',(-1.65+i*.05,-2.277,F+.972),(.019,.009,.002),mark,.001)
# Slim extraction canopy with visible duct; no hidden construction language.
box('Kitchen extraction hood',(-1.49,-2.49,F+1.68),(.82,.54,.075),metal,.01)
for x in [-1.74,-1.24]:box('Hood warm task light',(x,-2.35,F+1.639),(.08,.12,.003),cream)
rod('Exposed kitchen extraction duct',(-1.49,-2.60,F+1.71),(-1.49,-2.60,F+2.33),.105,metal,sides=48)
# Timber shelving away from hob and exposed copper cookware rail.
box('Kitchen upper hardwood shelf',(-3.96,-2.72,F+1.84),(2.77,.32,.05),wood,.008)
rod('Copper cookware hanging rail',(-5.1,-2.57,F+1.52),(-2.75,-2.57,F+1.52),.014,copperpot,sides=20)
for x in [-5.04,-2.81]:rod('Cookware rail wall bracket',(x,-2.89,F+1.52),(x,-2.57,F+1.52),.012,metal,sides=16)
# Hollow copper vessels with stainless lining; induction-compatible base concept.
def vessel(n,x,y,z,r,h):
N=64;profile=[(0,0),(r*.83,0),(r,.025),(r,h),(r-.012,h+.003),(r-.016,.036),(0,.036)]
vs=[(x+rr*cos(i*2*pi/N),y+rr*sin(i*2*pi/N),z+zz) for rr,zz in profile for i in range(N)]
fs=[]
for j in range(len(profile)-1):
for i in range(N):fs.append((j*N+i,j*N+(i+1)%N,(j+1)*N+(i+1)%N,(j+1)*N+i))
ob=mesh(n,vs,fs,copperpot);ob.data.materials.append(stainless)
for p in ob.data.polygons:p.use_smooth=True;p.material_index=1 if p.index//N>=3 else 0
rod(n+' induction compatible base concept',(x,y,z-.003),(x,y,z+.004),r*.85,stainless,sides=64)
horizontal_ring(n+' rolled rim',x,y,z+h,r-.005,.006,stainless)
return ob
vessel('COPPER STOCKPOT',-2.91,-2.50,F+.954,.18,.235)
for x in [-3.13,-2.69]:
rod('Stockpot handle',(x,-2.57,F+1.12),(x,-2.43,F+1.12),.012,metal,sides=16)
for y in [-2.57,-2.43]:rod('Stockpot handle fixing',(x,y,F+1.12),(x+(.055 if x<-2.9 else -.055),y,F+1.12),.009,metal,sides=12)
vessel('COPPER SAUCEPAN',-1.32,-2.40,F+.977,.09,.09)
rod('Saucepan handle',(-1.22,-2.40,F+1.05),(-.95,-2.40,F+1.09),.012,metal,sides=20)
vessel('COPPER FRYING PAN',-3.45,.02,F+.965,.155,.037)
rod('Frying pan long handle',(-3.29,.02,F+1.0),(-3.0,.02,F+1.03),.013,metal,sides=20)
# Marble-topped island: central prep surface clear of the dining table.
ix,iy=-3.8,.10
box('ISLAND - warm timber cabinet body',(ix,iy,F+.44),(2.05,.83,.88),wood,.012)
box('ISLAND - recessed plinth',(ix,iy,F+.06),(1.93,.73,.12),steel,.007)
box('ISLAND - honed marble countertop',(ix,iy,F+.94),(2.2,.95,.06),marble,.012)
for x in [ix-.68,ix,ix+.68]:
for z in [.30,.69]:
box('Island drawer front',(x,iy-.425,F+z),(.657,.028,.36),wood,.006)
rod('Island drawer pull',(x-.14,iy-.448,F+z+.09),(x+.14,iy-.448,F+z+.09),.009,metal,sides=16)
for i in range(29):box('Island north face timber slat',(ix-.98+i*.07,iy+.425,F+.49),(.042,.023,.75),woodz,.004)
box('Island end grain chopping board',(-4.37,.14,F+.985),(.45,.34,.03),wood,.015)
# Tea crockery and pantry objects add scale without filling the work surface.
for x in [-5.03,-4.67,-4.30]:
vessel('Shelf ceramic storage vessel',x,-2.71,F+1.87,.085,.18)
for ob in list(col.objects):
if ob.name.startswith('Shelf ceramic storage vessel') and ob.type=='MESH':
for slot in range(len(ob.data.materials)):ob.data.materials[slot]=cream
# Rebuild only the old furniture proxy so replaced kitchen blocks disappear in fast view.
furniture=bpy.data.collections.get('10 Furniture');fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
oldproxy=bpy.data.objects.get('FAST | 10 Furniture')
if oldproxy and furniture:
vs=[];fs=[];indices=[];materials=[];mapping={}
for ob in furniture.objects:
if ob.type!='MESH' or ob.hide_render:continue
remap=[]
for m in ob.data.materials:
if m not in mapping:mapping[m]=len(materials);materials.append(m)
remap.append(mapping[m])
world=Matrix.LocRotScale(ob.location.copy(),ob.rotation_euler.to_quaternion(),ob.scale.copy());off=len(vs);vs.extend(tuple(world@v.co) for v in ob.data.vertices)
for poly in ob.data.polygons:fs.append(tuple(off+j for j in poly.vertices));indices.append(remap[poly.material_index])
me=bpy.data.meshes.new('Furniture after kitchen replacement');me.from_pydata(vs,[],fs);me.update()
for m in materials:me.materials.append(m)
me.polygons.foreach_set('material_index',indices);oldproxy.data=me
kitchen_checks={'oven':1,'induction_zones':4,'copper_stockpot':1,'copper_saucepan':1,'copper_frying_pan':1,'marble_island_m':[2.2,.95],'island_rear_work_aisle_m':1.67,'old_simplified_kitchen_removed':True,'fast_furniture_refreshed':bool(oldproxy)}
s['Kitchen design']='Honed marble island, detailed timber drawers, oven, four-zone induction, extraction, hollow copper cookware with stainless food-contact lining and an induction-compatible base concept.'

View File

@@ -0,0 +1,71 @@
import bpy,os,re,json,bmesh
from mathutils import Vector,Matrix
OUT=os.path.abspath(os.path.join(os.path.dirname(__file__),'../samford-meditation-unreal'));os.makedirs(OUT+'/Textures',exist_ok=True)
source=bpy.context.scene
detail=bpy.data.collections.get('DETAIL - full geometry for renders')
if detail:detail.hide_viewport=False
source.frame_set(2220);bpy.context.view_layer.update()
origin=Vector((-11,12,1.12))
objects=list(bpy.data.collections['23 NOW - Quiet meditation room'].objects)
objects += [o for o in bpy.data.collections['20 Spacious site connections'].objects if o.name.startswith('Meditation room')]
objects=[o for o in objects if o.type=='MESH' and not o.name.startswith(('Meditation covered walk','Meditation walk'))]
deps=bpy.context.evaluated_depsgraph_get()
new=bpy.data.scenes.new('Meditation room - Unreal transfer');new.unit_settings.system='METRIC';new.unit_settings.scale_length=1
manifest={'source':'Samford solarpunk homestead','origin_blender_metres':list(origin),'room_metres':[4.5,4.8],'objects':[],'materials':{},'camera_blender_metres':[1.7,2.0,1.88],'target_blender_metres':[-.5,-.5,.43],'camera_lens_mm':25,'render_status':'Prepared for Unreal; no Unreal render has been produced by this exporter.'}
new_objects=[];used=[]
for i,o in enumerate(objects):
ev=o.evaluated_get(deps);me=bpy.data.meshes.new_from_object(ev,preserve_all_data_layers=True,depsgraph=deps)
me.transform(Matrix.Translation(-origin)@o.matrix_world)
ob=bpy.data.objects.new('SM_%03d_'%i+re.sub('[^a-zA-Z0-9_]','_',o.name),me);new.collection.objects.link(ob);new_objects.append(ob)
for mat in me.materials:
if mat and mat not in used:used.append(mat)
manifest['objects'].append({'name':ob.name,'source':o.name})
bpy.context.window.scene=new
# Unwrap the small room assets; source procedural shaders remain in the original file.
for o in new_objects:
bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o
bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.uv.smart_project(island_margin=.025);bpy.ops.object.mode_set(mode='OBJECT')
new.render.engine='CYCLES';new.cycles.device='CPU';new.cycles.samples=1;new.render.bake.margin=4
bpy.ops.object.select_all(action='DESELECT');bpy.ops.mesh.primitive_plane_add(size=2);tile=bpy.context.object;tile.name='Temporary material baking tile'
replacement={}
for i,m in enumerate(used):
key='M_%02d_'%i+re.sub('[^a-zA-Z0-9_]','_',m.name)
p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None) if m.use_nodes else None
base=list(m.diffuse_color);rough=float(p.inputs['Roughness'].default_value) if p else .7;metal=float(p.inputs['Metallic'].default_value) if p else 0
record={'source':m.name,'base_color':base,'roughness':rough,'metallic':metal,'emission':0,'transmission':0}
if p:
record['emission']=float(p.inputs['Emission Strength'].default_value)
record['transmission']=float(p.inputs['Transmission Weight'].default_value)
exported=bpy.data.materials.new(key);exported.use_nodes=True;ep=exported.node_tree.nodes.get('Principled BSDF');ep.inputs['Base Color'].default_value=base;ep.inputs['Roughness'].default_value=rough;ep.inputs['Metallic'].default_value=metal
if p and p.inputs['Base Color'].is_linked:
bake=m.copy();tile.data.materials.clear();tile.data.materials.append(bake);nodes=bake.node_tree.nodes;links=bake.node_tree.links;bp=next(n for n in nodes if n.type=='BSDF_PRINCIPLED');output=next(n for n in nodes if n.type=='OUTPUT_MATERIAL')
im=bpy.data.images.new(key+'_BaseColor',width=512,height=512);im.filepath_raw=OUT+'/Textures/'+key+'_BaseColor.png';im.file_format='PNG'
target=nodes.new('ShaderNodeTexImage');target.image=im;nodes.active=target
emission=nodes.new('ShaderNodeEmission');links.new(bp.inputs['Base Color'].links[0].from_socket,emission.inputs['Color']);links.new(emission.outputs[0],output.inputs['Surface'])
bpy.ops.object.select_all(action='DESELECT');tile.select_set(True);bpy.context.view_layer.objects.active=tile;bpy.ops.object.bake(type='EMIT');im.save()
tex=exported.node_tree.nodes.new('ShaderNodeTexImage');tex.image=im;exported.node_tree.links.new(tex.outputs['Color'],ep.inputs['Base Color']);record['base_color_texture']='Textures/'+key+'_BaseColor.png'
if bp.inputs['Normal'].is_linked:
normal_image=bpy.data.images.new(key+'_Normal',width=512,height=512)
normal_image.colorspace_settings.name='Non-Color';normal_image.filepath_raw=OUT+'/Textures/'+key+'_Normal.png';normal_image.file_format='PNG'
target.image=normal_image;nodes.active=target;links.new(bp.outputs['BSDF'],output.inputs['Surface'])
bpy.ops.object.bake(type='NORMAL');normal_image.save()
normal_tex=exported.node_tree.nodes.new('ShaderNodeTexImage');normal_tex.image=normal_image
normal_node=exported.node_tree.nodes.new('ShaderNodeNormalMap');exported.node_tree.links.new(normal_tex.outputs['Color'],normal_node.inputs['Color']);exported.node_tree.links.new(normal_node.outputs['Normal'],ep.inputs['Normal'])
record['normal_texture']='Textures/'+key+'_Normal.png'
record['normal_convention']='OpenGL +Y'
bpy.data.materials.remove(bake)
replacement[m]=exported;manifest['materials'][key]=record
print('MATERIAL_READY '+key,flush=True)
for o in new_objects:
for slot in o.material_slots:
if slot.material in replacement:slot.material=replacement[slot.material]
bpy.data.objects.remove(tile,do_unlink=True)
# This deliberately keeps the viewing face open for the simple room render.
bpy.ops.object.select_all(action='DESELECT')
for o in new_objects:o.select_set(True)
bpy.context.view_layer.objects.active=new_objects[0]
bpy.ops.export_scene.fbx(filepath=OUT+'/MeditationRoom.fbx',use_selection=True,object_types={'MESH'},use_mesh_modifiers=False,add_leaf_bones=False,bake_anim=False,axis_forward='-Y',axis_up='Z',path_mode='RELATIVE')
bpy.ops.export_scene.gltf(filepath=OUT+'/MeditationRoom.glb',export_format='GLB',use_selection=True,export_animations=False)
json.dump(manifest,open(OUT+'/meditation-room-manifest.json','w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/meditation-room-transfer.blend')
print('MEDITATION_EXPORT_READY '+str(len(new_objects))+' meshes, '+str(len(used))+' materials',flush=True)

View File

@@ -0,0 +1,60 @@
import bpy, bmesh, math, json, os
from mathutils import Vector
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
# Clear grass from the new vehicle surfaces and garage interior.
route=[(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)]
segments=[]
for a,b in zip(route,route[1:]):
dx,dy=b[0]-a[0],b[1]-a[1]; segments.append((a[0],a[1],dx,dy,dx*dx+dy*dy))
def clear(x,y):
if -7.2<x<1.2 and -22.7<y<-15.3:return True
if -7.05<x<5.05 and -15.55<y<-9.45:return True
for ax,ay,dx,dy,l2 in segments:
t=max(0,min(1,((x-ax)*dx+(y-ay)*dy)/l2))
if (x-ax-t*dx)**2+(y-ay-t*dy)**2<2.08**2:return True
return False
removed=0
for o in s.objects:
if o.type=='MESH' and o.name.startswith('Native grasses'):
bm=bmesh.new(); bm.from_mesh(o.data); faces=[]
for f in bm.faces:
p=o.matrix_world@f.calc_center_median()
if clear(p.x,p.y):faces.append(f)
removed+=len(faces); bmesh.ops.delete(bm,geom=faces,context='FACES'); bm.to_mesh(o.data); bm.free()
# This broad panel is the vehicle entrance, not a permanent wall across the car bay.
o=bpy.data.objects.get('Garage north front panel')
if o:o.name='Garage 3.7m wide sectional vehicle door'; o['Operation']='Concept upward-opening vehicle door; transparent for presentation.'
o=bpy.data.objects.get('Garage roller door concept')
if o:o.name='Garage north service infill panel'
# Distinct static overview for inspecting the driveway without changing the Play camera.
camdata=bpy.data.cameras.new('VIEW - Road garage and retreat'); cam=bpy.data.objects.new(camdata.name,camdata); s.collection.objects.link(cam)
cam.location=(47,40,47); cam.rotation_euler=(Vector((0,-7,1))-cam.location).to_track_quat('-Z','Y').to_euler(); camdata.lens=38; camdata.clip_end=450
tour=bpy.data.objects['PLAY TOUR camera']; s.camera=tour; s.frame_set(1)
report=json.load(open(os.path.join(OUT,'design-checks.json'))); report['grass_faces_cleared_from_vehicle_surfaces']=removed
report['retained_child_beds']=sum(o.name.startswith('Child') and ('bed' in o.name or 'mattress' in o.name) for o in s.objects)
report['transparent_buildings']=['Living pavilion','Parents bedroom wing','Short corridor','Meditation room','Sauna','Greenhouse','Garage']
assert report['retained_child_beds']==0 and report['candles']==3
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))
for ch in chapters:
s.frame_set(ch['start']); a=tour.location.copy(); s.frame_set(ch['end']); b=tour.location.copy(); assert (b-a).length>.01
report['all_23_chapters_animate']=True; json.dump(report,open(os.path.join(OUT,'design-checks.json'),'w'),indent=2)
s.camera=tour; s.frame_set(1)
for screen in bpy.data.screens:
for area in screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.region_3d.view_perspective='CAMERA'; area.spaces.active.shading.type='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.overlay.show_overlays=False
s.render.engine='CYCLES'; s.render.resolution_percentage=100; s.cycles.samples=48; s.cycles.use_denoising=True; s.cycles.adaptive_threshold=.035
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU'; s.render.filepath=os.path.join(OUT,'retreat-garage-overview.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
# Captions are useful during playback, but photographs use clean architectural views.
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
for o in titles:o.hide_render=True
s.camera=cam; bpy.ops.render.render(write_still=True); print('OVERVIEW_RENDERED',flush=True)
s.camera=tour; s.frame_set(2220); s.render.filepath=os.path.join(OUT,'meditation-room.png'); bpy.ops.render.render(write_still=True); print('MEDITATION_RENDERED',flush=True)
s.frame_set(2460); s.render.filepath=os.path.join(OUT,'garage-workstation-bikes.png'); bpy.ops.render.render(write_still=True); print('GARAGE_RENDERED',flush=True)
for o in titles:o.hide_render=False
s.camera=tour; s.frame_set(1); s.render.filepath=os.path.join(OUT,'retreat-garage-overview.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
print('FINAL_RETREAT_READY_TO_PLAY',flush=True)

View File

@@ -0,0 +1,11 @@
{
"deleted_objects": 143,
"moved_objects": 1655,
"flower_beds": 13,
"planted_clumps": 221,
"flowering_shrubs": 8,
"smooth_routes": 13,
"table_tennis_removed": true,
"future_pavilions": 2,
"tour_chapters": 30
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"no_table_tennis": true,
"garage_center": [
0.0,
-19.0
],
"meditation_center": [
-11.8,
12.0
],
"caption_issues": [],
"tour_chapters": 30,
"rainwater_tanks": 2,
"kitchen_preserved": true,
"flower_beds": 13,
"render_size": [
2560,
1440,
100
],
"fast_excluded_from_render": true,
"pavilion_path_intersections": [],
"solar_tree_clearance": true
}

View File

@@ -0,0 +1,25 @@
import bpy,os
from mathutils import Vector
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene; c=bpy.data.collections['23 NOW - Quiet meditation room']
# Keep the entrance clear and support the low stone candle shelf on matching stone feet.
for o in c.objects:
if o.name.startswith(('Low stone candle shelf','Stone candle holder','Meditation candle','Small candle flame','Soft candle glow')):o.location.x-=1.4
for x in [-12.9,-11.9]:
bpy.ops.mesh.primitive_cube_add(size=1,location=(x,10.36,1.235)); o=bpy.context.object; o.name='Low candle shelf stone foot'; o.dimensions=(.14,.30,.23); bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
for col in list(o.users_collection):col.objects.unlink(o)
c.objects.link(o); o.data.materials.append(bpy.data.materials['Split sandstone facing - mineral grain']); mod=o.modifiers.new('Soft stone edge','BEVEL'); mod.width=.008; mod.segments=2
cam=bpy.data.objects['PLAY TOUR camera']
for f,p in [(2161,(-9.3,14.0,3.0)),(2280,(-9.6,13.5,2.8))]:
cam.location=p; cam.rotation_euler=(Vector((-11.5,11.5,1.55))-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=f); cam.keyframe_insert('rotation_euler',frame=f); cam.data.lens=25; cam.data.keyframe_insert('lens',frame=f)
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT' and o.name.startswith('19 /'):half=36/25; o.location=(-half*.91,half*9/16*.84,-2)
s.frame_set(1); s.camera=cam
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
for o in titles:o.hide_render=True
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU'; s.cycles.samples=40; s.frame_set(2220); s.render.filepath=os.path.join(OUT,'meditation-room.png'); bpy.ops.render.render(write_still=True)
for o in titles:o.hide_render=False
s.frame_set(1); s.render.filepath=os.path.join(OUT,'retreat-garage-overview.png'); bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
print('MEDITATION_FINAL_READY',flush=True)

View File

@@ -0,0 +1,100 @@
import bpy,json,time,statistics,os
from mathutils import Vector
OUT=os.path.dirname(os.path.abspath(__file__))
s=bpy.context.scene
original=list(s.objects)
visible=[o for o in original if o.visible_get()]
render_flags={o:o.hide_render for o in original}
render_settings=(s.render.engine,s.render.resolution_percentage,s.render.filepath)
def preview(tag,objects):
for o in s.objects:o.hide_render=o not in objects
s.render.engine='BLENDER_WORKBENCH';s.render.resolution_percentage=40
s.display.shading.light='STUDIO';s.display.shading.color_type='MATERIAL'
s.display.shading.show_shadows=False;s.display.shading.show_cavity=False
s.display.shading.show_specular_highlight=False
times=[]
for f in [1,60,2460]:
s.frame_set(f);s.render.filepath=os.path.join(OUT,tag+('-garage' if f==2460 else '')+'.png')
t=time.perf_counter();bpy.ops.render.render(write_still=f!=60);times.append(time.perf_counter()-t)
return times
base_objects={o for o in visible if o.display_type not in {'WIRE','BOUNDS'}}
baseline=preview('performance-before',base_objects)
for o,flag in render_flags.items():o.hide_render=flag
s.render.engine=render_settings[0]
s.frame_set(1)
fast=bpy.data.collections.new('00 FAST TOUR - lightweight viewport');s.collection.children.link(fast)
fast.hide_render=True
def combined(name,objects):
verts=[];faces=[];indices=[];smooth=[];materials=[];mat_map={}
for o in objects:
me=o.data;is_leaf='individual canopy leaves' in o.name
mi=[]
for mat in me.materials:
if mat not in mat_map:
mat_map[mat]=len(materials);materials.append(mat)
mi.append(mat_map[mat])
def material_idx(i):return mi[i] if mi and i<len(mi) else 0
if is_leaf:
# Keep one complete leaf in four. Flatten its four tiny triangles
# into one quad and enlarge slightly to preserve canopy coverage.
assert len(me.vertices)%5==0 and len(me.polygons)==len(me.vertices)//5*4
for k in range(0,len(me.vertices),20):
c=sum((me.vertices[k+j].co for j in range(4)),Vector())/4
off=len(verts)
verts.extend(tuple(o.matrix_world@(c+(me.vertices[k+j].co-c)*1.55)) for j in range(4))
faces.append((off,off+1,off+2,off+3));indices.append(material_idx(me.polygons[k//5*4].material_index));smooth.append(False)
else:
off=len(verts);verts.extend(tuple(o.matrix_world@v.co) for v in me.vertices)
faces.extend(tuple(off+i for i in p.vertices) for p in me.polygons)
indices.extend(material_idx(p.material_index) for p in me.polygons)
smooth.extend(p.use_smooth for p in me.polygons)
if not faces:return
me=bpy.data.meshes.new(name);me.from_pydata(verts,[],faces);me.update()
for m in materials:me.materials.append(m)
me.polygons.foreach_set('material_index',indices);me.polygons.foreach_set('use_smooth',smooth)
ob=bpy.data.objects.new(name,me);fast.objects.link(ob)
ob['Purpose']='Lightweight static tour display. Original editable geometry is retained in the DETAIL collection and used for renders.'
ob['Source objects']=len(objects)
return ob
seen=set()
for col in list(bpy.data.collections):
if col==fast:continue
obs=[o for o in col.objects if o in base_objects and o.type=='MESH' and o not in seen and not o.animation_data and not o.parent]
if not obs:continue
combined('FAST | '+col.name,obs);seen.update(obs)
# Originals remain fully editable, with all materials and render visibility intact.
# Globally disabling their collection affects only viewport evaluation.
detail=bpy.data.collections.new('DETAIL - full geometry for renders');s.collection.children.link(detail)
for col in list(s.collection.children):
if col in {fast,detail} or col.name.startswith('22 PLAY'):continue
s.collection.children.unlink(col);detail.children.link(col)
detail.hide_viewport=True
for screen in bpy.data.screens:
for a in screen.areas:
if a.type=='VIEW_3D':
sp=a.spaces.active;sp.shading.type='SOLID';sp.shading.light='STUDIO';sp.shading.color_type='MATERIAL'
sp.shading.show_cavity=False;sp.shading.show_shadows=False;sp.shading.show_specular_highlight=False
sp.overlay.show_overlays=False;sp.region_3d.view_perspective='CAMERA'
s.sync_mode='FRAME_DROP';s.cycles.preview_samples=8
s['Viewport performance']='FAST TOUR provides simplified static meshes for navigation. Detailed geometry is retained under DETAIL, disabled only in the viewport. Final Cycles renders automatically use DETAIL and omit FAST TOUR.'
s['Editing full detail']='In the Outliner filter menu show Disable in Viewports (monitor icon). Enable DETAIL and disable FAST TOUR to edit the full model. Return to FAST TOUR for playback.'
fast_objects=set(fast.objects)|{o for o in original if o.type in {'CAMERA','FONT'} and o in base_objects}
# Make preview-only proxies available to the independent Workbench test.
fast.hide_render=False
optimized=preview('performance-after',fast_objects)
fast.hide_render=True
for o,flag in render_flags.items():o.hide_render=flag
for o in fast.objects:o.hide_render=False
s.render.engine=render_settings[0];s.render.resolution_percentage=render_settings[1];s.render.filepath=render_settings[2]
s.frame_set(1);bpy.context.view_layer.update()
times=[]
for f in range(1,97):
t=time.perf_counter();s.frame_set(f);bpy.context.view_layer.update();times.append(time.perf_counter()-t)
now=[o for o in s.objects if o.visible_get()]
report={'before_visible_objects':len(visible),'after_visible_objects':len(now),'before_visible_faces':sum(len(o.data.polygons) for o in visible if o.type=='MESH'),'after_visible_faces':sum(len(o.data.polygons) for o in now if o.type=='MESH'),'workbench_before_seconds':baseline,'workbench_after_seconds':optimized,'after_frame_update_ms_median':statistics.median(times)*1000,'original_render_visibility_preserved':all(o.hide_render==v for o,v in render_flags.items()),'original_objects_preserved':all(o.name in s.objects for o in original),'fast_geometry_excluded_from_renders':fast.hide_render,'detailed_geometry_disabled_only_in_viewport':detail.hide_viewport and not detail.hide_render}
assert report['original_render_visibility_preserved'] and report['original_objects_preserved'] and report['fast_geometry_excluded_from_renders'] and report['detailed_geometry_disabled_only_in_viewport']
json.dump(report,open(os.path.join(OUT,'performance-checks.json'),'w'),indent=2)
s.frame_set(1);s.camera=bpy.data.objects['PLAY TOUR camera']
filename='samford-solarpunk-smooth-tour.blend' if bpy.data.objects.get('TESLA MODEL Y - pearl white flowing body') else 'samford-retreat-smooth-tour.blend'
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,filename))
print('PERFORMANCE_RESULT '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,26 @@
{
"before_visible_objects": 4090,
"after_visible_objects": 47,
"before_visible_faces": 1911296,
"after_visible_faces": 197676,
"workbench_before_seconds": [
1.7875650000005407,
1.0031288749996747,
1.2839702920009586
],
"workbench_after_seconds": [
0.21899866700005077,
0.16925074999926437,
0.17747099999905913
],
"after_frame_update_ms_median": 0.17172900061268592,
"original_render_visibility_preserved": true,
"original_objects_preserved": true,
"fast_geometry_excluded_from_renders": true,
"detailed_geometry_disabled_only_in_viewport": true,
"all_23_tour_chapters_verified_after_reload": true,
"tesla_present": true,
"garage_solar_modules": 8,
"ev_charger_present": true,
"full_detail_final_render": "Render flags and geometry retained; checked with a Cycles garage render."
}

View File

@@ -0,0 +1,8 @@
{
"books": 163,
"bookshelf_bays": 2,
"reading_chairs": 1,
"tour_chapters": 24,
"tour_seconds": 120,
"fast_proxy_objects_added": 1
}

View File

@@ -0,0 +1,234 @@
import bpy,math,json,os,random
from mathutils import Vector,Matrix
from math import sin,cos,pi
OUT=os.path.dirname(__file__);s=bpy.context.scene;random.seed(904)
detail=bpy.data.collections['DETAIL - full geometry for renders'];fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
col=bpy.data.collections.new('34 Garden court - aligned pavilions and flowing landscape');detail.children.link(col)
COL={'new':col};active='new';src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];roof=bpy.data.materials['Pale Colorbond - powder coated steel'];gravel=bpy.data.materials['Loose local gravel - fine stones'];soil=bpy.data.materials['Damp brown garden soil'];F=1.12
leafm=[mat('Garden leaf '+str(i),c,.77) for i,c in enumerate([(.12,.24,.085),(.21,.32,.12),(.15,.29,.21),(.28,.36,.15)])]
flowers=[mat('Garden flower '+n,c,.6) for n,c in [('soft ivory',(.89,.86,.72)),('warm lilac',(.45,.23,.55)),('bottlebrush rose',(.60,.10,.19)),('native yellow',(.83,.53,.075))]]
lawn=mat('Courtyard low native groundcover',(.18,.26,.105),.95)
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
def matrix(o):return Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
def center(o):return matrix(o)@(sum((Vector(v) for v in o.bound_box),Vector())/8) if o.type=='MESH' else o.location.copy()
dirty=set();removed=[];moved=set()
def delete(o):
dirty.update(c.name for c in o.users_collection);removed.append(o.name);bpy.data.objects.remove(o,do_unlink=True)
def move(o,delta):
if o in moved:return
dirty.update(c.name for c in o.users_collection);o.location+=Vector(delta);moved.add(o)
# Remove the entire play platform and its equipment, old angular paths and links.
for o in list(bpy.data.collections['33 Daily life - training gathering play and food'].objects):
n=o.name
if n.startswith(('PLAY DECK','FOLDING TABLE TENNIS','Table tennis','Play deck','Training to garage pedestrian link','SHADED WALKING LOOP','Walking loop')):delete(o)
elif center(o).x<-7 and center(o).y<-13:move(o,(2.8,-1,0))
for cname in ['20 Spacious site connections','27 Gravel access road and arrival','23 NOW - Quiet meditation room']:
for o in list(bpy.data.collections[cname].objects):
if cname.startswith('27') or o.name in ['Main meandering gravel arrival','Sauna and tanks gravel walk','Garden and greenhouse gravel walk'] or o.name.startswith(('Meditation covered walk','Meditation walk timber')):delete(o)
# Align meditation with parents wing; line the garage up with main pavilion axis.
for cname,delta in [('23 NOW - Quiet meditation room',(-.8,0,0)),('28 Garage and workbench',(3,0,0)),('29 Car and bicycles',(3,0,0)),('16 Oval Finnish sauna',(0,2.2,0)),('17 Outdoor shower and stone terrace',(0,2.2,0))]:
for o in bpy.data.collections[cname].objects:move(o,delta)
for o in bpy.data.collections['30 VIEW - Transparent building sides'].objects:
if o.name.startswith('Meditation'):move(o,(-.8,0,0))
elif o.name.startswith('Garage'):move(o,(3,0,0))
elif o.name.startswith('Sauna'):move(o,(0,2.2,0))
for o in bpy.data.collections['20 Spacious site connections'].objects:
if o.name.startswith('Meditation'):move(o,(-.8,0,0))
# Shift only ground-mounted solar; retain roof PV, tanks and battery cabinet.
for o in bpy.data.collections['05 Water and services'].objects:
p=center(o)
if p.x>8 and p.y<-12:move(o,(-3.8,-1.3,0))
# Relocate two concept eucalyptus groups clear of solar and the curved driveway.
tree_delta=None
for o in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
if o.name.startswith(('Eucalyptus pale trunk','Fruit tree trunk')):
tree_delta=(-1,-11,0) if o.name=='Eucalyptus pale trunk.006' else ((7,-1,0) if o.name=='Eucalyptus pale trunk.007' else None)
if tree_delta:move(o,tree_delta)
# Recompose future shadow modules as a mirrored partner and an axial northern reserve.
for o in bpy.data.collections['24 LATER - Shadow spaces only'].objects:
p=center(o);move(o,(14.8,-5,0) if p.x<1 else (-7,9,0))
# Smooth cubic paths, sampled once and shared with the plan drawing.
paths=[];beds=[]
def smooth(points,closed=False,n=20):
p=[Vector(q) for q in points];out=[]
for i in range(len(p) if closed else len(p)-1):
a=p[(i-1)%len(p)] if closed or i else p[0];b=p[i];c=p[(i+1)%len(p)];d=p[(i+2)%len(p)] if closed else p[min(i+2,len(p)-1)]
for j in range(n):
t=j/n;v=.5*((2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t);out.append(tuple(v))
out.append(out[0] if closed else tuple(p[-1]));return out
def ribbon(name,pts,w,material,z=None,record=True):
vs=[];fs=[]
for i,p in enumerate(pts):
p=Vector(p);d=Vector(pts[min(i+1,len(pts)-1)])-Vector(pts[max(0,i-1)]);side=Vector((-d.y,d.x)).normalized()*w/2
for q in [p-side,p+side]:vs.append((q.x,q.y,ground(q.x,q.y)+.085 if z is None else z))
for i in range(len(pts)-1):k=i*2;fs.append((k,k+1,k+3,k+2))
ob=mesh(name,vs,fs,material)
if record:paths.append({'name':name,'points':pts,'width':w,'kind':'roofed walk' if z is not None else 'gravel'})
return ob
def route(name,points,w=1.35,closed=False):return ribbon(name,smooth(points,closed),w,gravel)
def ellipse(cx,cy,rx,ry,n=128):return [(cx+rx*cos(i*2*pi/n),cy+ry*sin(i*2*pi/n)) for i in range(n+1)]
def patch(name,pts,material):
cx=sum(p[0] for p in pts[:-1])/(len(pts)-1);cy=sum(p[1] for p in pts[:-1])/(len(pts)-1)
vs=[(cx,cy,ground(cx,cy)+.045)]+[(x,y,ground(x,y)+.045) for x,y in pts[:-1]]
return mesh(name,vs,[(0,i+1,(i+1)%(len(vs)-1)+1) for i in range(len(vs)-1)],material)
# Central oval garden with balanced paired flower beds and clear north view.
patch('CENTRAL GARDEN - low meadow oval',ellipse(0,13.2,7.8,4.6),lawn)
ribbon('GARDEN COURT - continuous oval gravel walk',ellipse(0,13.2,8.5,5.3),1.35,gravel)
# Covered curving timber connection to the existing meditation entry.
walk=smooth([(-3,5.4),(-4,6.6),(-7.8,7.0),(-10.8,7.9),(-11.8,9.6)])
ribbon('CURVED BREEZEWAY - timber deck',walk,1.35,wood,F)
ribbon('Curved breezeway pale roof',walk,1.55,roof,F+2.60,False)
for i in range(0,len(walk),10):
x,y=walk[i];p=Vector(walk[min(i+1,len(walk)-1)])-Vector(walk[max(0,i-1)]);side=Vector((-p.y,p.x)).normalized()
for sign in [-1,1]:
q=Vector((x,y))+side*.61*sign;g=ground(*q);box('Curved walk timber post',(q.x,q.y,(g+F+2.60)/2),(.085,.085,F+2.6-g),woodz,.006)
beam('Curved walk radial rafter',(x-side.x*.78,y-side.y*.78,F+2.56),(x+side.x*.78,y+side.y*.78,F+2.56),.055,.13,wood)
for i in range(0,len(walk)-1,2):
x,y=walk[i];p=Vector(walk[i+1])-Vector(walk[i]);side=Vector((-p.y,p.x)).normalized();beam('Curved walkway individual deck board',(x-side.x*.66,y-side.y*.66,F-.025),(x+side.x*.66,y+side.y*.66,F-.025),.115,.05,wood)
# Broad, shallow stair centred on the main pavilion.
for i in range(8):
top=F-.16*(i+1);box('Garden axis broad timber stair',(0,5.57+i*.30,top-.075),(2.4,.32,.15),wood,.008)
route('Garden court east future threshold',[(8.5,13.2),(9.55,13.2)])
route('Northern axial future path',[(0,18.5),(0,20),(0,21.6)])
route('Wellness garden curve',[(-7,16.2),(-12,17),(-17,16),(-18,10),(-20,8.3),(-23,8.1),(-23,7.3)],1.4)
route('Rainwater service garden path',[(-23,8.1),(-28.5,8.2),(-29.3,1),(-28,-5),(-25,-8)],1.15)
route('Eastern garden promenade',[(7,16.2),(12,17),(17,16),(20,11.5),(21,7),(21,1),(21,-5),(24.2,-4.8)],1.4)
route('Orchard quiet loop',[(20,11.5),(27,12.5),(32,10),(33,3),(33,-8),(28,-11),(22,-10),(20,-5),(21,1)],1.25)
route('Dining terrace to food garden',[(15,3.5),(17.5,4.8),(21,4.8)],1.4)
route('Kitchen garden harvest access',[(28.8,4.8),(30.8,4),(31,1.5),(30.2,1.5)],1.0)
# Generous pedestrian arrival curves around the planted former play footprint.
route('Arrival garden promenade',[(3.2,-13.8),(5.6,-10.5),(6.3,-5.2),(10,-2),(15,-.2),(15,3.5)],1.5)
route('Training arrival link',[(-5.9,-18),(-5.5,-15.2),(-4.7,-11.3),(-1,-9.8),(5.6,-10.5)],1.25)
for i in range(6):
top=F-.18*(i+1);box('Dining terrace garden access stair',(13.16+i*.30,3.5,top-.08),(.32,1.5,.16),wood,.008)
road=smooth([(36,-40),(28,-33),(20,-27),(16.8,-22),(16,-16),(12,-12.3),(6,-12.3),(0,-12.3)])
ribbon('VEHICLE ACCESS - flowing 4m gravel driveway',road,4,gravel)
apron=smooth([(-4,-14.5),(-3,-15.4),(6,-15.4),(8,-14),(8,-10.4),(6,-9.5),(-3,-9.5),(-4,-10.5)],True)
patch('Rounded gravel garage arrival court',apron,gravel)
# Low and medium native-inspired planting; all beds outside buildings and walking surfaces.
# Geometry is illustrative of form, not a botanical planting specification.
flower_centers=[]
def gardenbed(name,cx,cy,rx,ry,seed):
rng=random.Random(seed);outline=ellipse(cx,cy,rx,ry);patch(name+' mulched soil',outline,soil);beds.append({'name':name,'center':[cx,cy],'radii':[rx,ry]})
# Fine timber edging follows the same soft geometry.
ribbon(name+' curved timber edge',outline,.045,wood,record=False)
vs=[];fs=[];mis=[]
def face(coords,mi):
k=len(vs);vs.extend(coords);fs.append(tuple(range(k,k+len(coords))));mis.append(mi)
for j in range(max(12,int(rx*ry*7))):
a=rng.random()*2*pi;rr=math.sqrt(rng.random())*.91;x=cx+rx*rr*cos(a);y=cy+ry*rr*sin(a);g=ground(x,y)+.06;h=rng.uniform(.2,.62)
flower_centers.append((x,y))
# Narrow arching lomandra/flax leaves.
for k in range(13):
a=rng.random()*2*pi;leng=rng.uniform(.22,.53);w=rng.uniform(.017,.035);dx,dy=cos(a),sin(a)
start=(x,y,g);mid=(x+dx*leng*.4,y+dy*leng*.4,g+h);end=(x+dx*leng,y+dy*leng,g+h*.42)
face([start,(mid[0]-dy*w,mid[1]+dx*w,mid[2]),end,(mid[0]+dy*w,mid[1]-dx*w,mid[2])],rng.randrange(4))
# Fine stems with small flower clusters, varied ivory / lilac / rose / yellow.
for f in range(rng.randint(3,6)):
xx=x+rng.uniform(-.22,.22);yy=y+rng.uniform(-.22,.22);zz=g+h+rng.uniform(.03,.20);c=rng.randrange(4)
face([(xx-.003,yy,g),(xx+.003,yy,g),(xx+.003,yy,zz),(xx-.003,yy,zz)],1)
for petal in range(6):
a=petal*pi/3;length=rng.uniform(.035,.055);ux,uy=cos(a),sin(a);px,py=-uy,ux
face([(xx,yy,zz),(xx+ux*length*.65+px*.020,yy+uy*length*.65+py*.020,zz+.014),(xx+ux*length,yy+uy*length,zz+.005),(xx+ux*length*.65-px*.020,yy+uy*length*.65-py*.020,zz+.014)],4+c)
ob=mesh(name+' native grasses and flowering perennials',vs,fs)
for m in leafm+flowers:ob.data.materials.append(m)
ob.data.polygons.foreach_set('material_index',mis)
# Paired garden beds establish symmetry without a solid wall of planting.
for sign in [-1,1]:
gardenbed(('West' if sign<0 else 'East')+' veranda flower border',sign*3.9,6.2,2.25,.52,100+sign)
gardenbed(('West' if sign<0 else 'East')+' oval court flower crescent',sign*4.2,14.1,2.1,1.12,200+sign)
gardenbed(('West' if sign<0 else 'East')+' axial future garden',sign*3.9,20.9,1.35,.7,300+sign)
gardenbed('FORMER PLAY AREA - flowering kitchen garden',10.9,-6.8,3.1,2.7,404)
gardenbed('Dining garden soft edge',16,7.3,2.3,1.0,405)
gardenbed('Sauna aromatic flower garden',-18.8,4.8,1.3,1.55,406)
gardenbed('Quiet room west native garden',-15.8,12.0,1.0,2.0,407)
gardenbed('Arrival flowering island',2.1,-7.1,2.1,1.05,408)
gardenbed('Food garden pollinator border',29.8,6.7,2.1,.60,409)
gardenbed('Quiet bench flower garden',30,13.9,2.1,.8,410)
# Shrub-scale green foliage with rose bottlebrush-inspired flowers, grouped in selected beds.
for idx,(cx,cy) in enumerate([(9,-7),(12,-7),(11,-5),(16,7.5),(-18.8,4.8),(-15.8,11),(-15.8,13),(29.7,6.8)]):
rng=random.Random(700+idx);g=ground(cx,cy);vs=[];fs=[];mis=[]
for j in range(140):
a=rng.random()*2*pi;b=rng.random()*pi;rr=rng.uniform(.25,.66);x=cx+rr*cos(a)*sin(b);y=cy+rr*sin(a)*sin(b);z=g+.7+rr*cos(b)*.7;d=rng.uniform(.055,.12);k=len(vs)
vs.extend([(x-d*cos(a),y-d*sin(a),z),(x-.035*sin(a),y+.035*cos(a),z+.025),(x+d*cos(a),y+d*sin(a),z+.045),(x+.035*sin(a),y-.035*cos(a),z+.025)]);fs.append((k,k+1,k+2,k+3));mis.append(j%4)
for j in range(12):
a=j*2*pi/12;x=cx+.5*cos(a);y=cy+.5*sin(a);z=g+.9+rng.uniform(-.15,.2)
for n in range(12):
b=n*2*pi/12;k=len(vs);vs.extend([(x,y,z-.07),(x+.04*cos(b),y+.04*sin(b),z),(x,y,z+.08)]);fs.append((k,k+1,k+2));mis.append(4)
ob=mesh('Bottlebrush inspired flowering shrub '+str(idx+1),vs,fs)
for m in leafm+[flowers[2]]:ob.data.materials.append(m)
ob.data.polygons.foreach_set('material_index',mis)
# Refresh affected lightweight geometry, keeping the existing simplified tree canopy.
proxy_map={'33 Daily life - training gathering play and food':'FAST | Training gathering play and garden loop'}
def combine(c,name):
old=bpy.data.objects.get(name)
if old:bpy.data.objects.remove(old,do_unlink=True)
vs=[];fs=[];inds=[];smooths=[];mats=[];mapping={}
for ob in c.objects:
if ob.type!='MESH' or ob.hide_render or ob.hide_get():continue
remap=[]
for m in ob.data.materials:
if m not in mapping:mapping[m]=len(mats);mats.append(m)
remap.append(mapping[m])
mw=matrix(ob)
if 'individual canopy leaves' in ob.name:
for k in range(0,len(ob.data.vertices),20):
c0=sum((ob.data.vertices[k+j].co for j in range(4)),Vector())/4;off=len(vs);vs.extend(tuple(mw@(c0+(ob.data.vertices[k+j].co-c0)*1.55)) for j in range(4));fs.append((off,off+1,off+2,off+3));inds.append(remap[ob.data.polygons[k//5*4].material_index]);smooths.append(False)
continue
off=len(vs);vs.extend(tuple(mw@v.co) for v in ob.data.vertices)
for p in ob.data.polygons:fs.append(tuple(off+i for i in p.vertices));inds.append(remap[p.material_index]);smooths.append(p.use_smooth)
if not fs:return
me=bpy.data.meshes.new(name);me.from_pydata(vs,[],fs);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',inds);me.polygons.foreach_set('use_smooth',smooths);ob=bpy.data.objects.new(name,me);fast.objects.link(ob)
for cname in dirty:
c=bpy.data.collections.get(cname)
if c and not cname.startswith('30'):combine(c,proxy_map.get(cname,'FAST | '+cname))
combine(col,'FAST | Aligned garden court and flowers')
# Shift camera chapters with their moved subject; replace play chapter with landscape.
cam=bpy.data.objects['PLAY TOUR camera'];chapters=json.load(open(OUT+'/tour-chapters-daily-life.json'))
def fcurves(owner):
for layer in owner.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
yield from bag.fcurves
for number,delta in [(10,(0,2.2,0)),(11,(0,2.2,0)),(12,(0,2.2,0)),(15,(-3.8,-1.3,0)),(18,(-.8,0,0)),(19,(-.8,0,0)),(20,(3,0,0)),(21,(3,0,0)),(25,(2.8,-1,0))]:
c=chapters[number-1]
for fc in fcurves(cam):
if fc.data_path=='location':
for k in fc.keyframe_points:
if c['start']<=k.co.x<=c['end']:
d=delta[fc.array_index];k.co.y+=d;k.handle_left.y+=d;k.handle_right.y+=d
newshots={1:('01 / ONE CONNECTED GARDEN HOMESTEAD',(44,56,48),(42,54,46),(0,-5,0),37),22:('22 / BALANCED FUTURE PAVILIONS',(23,32,17),(22,30,16),(0,15,1),35),23:('23 / RECTANGLES AND GARDEN CURVES',(0,38,30),(0,36,28),(0,4,0),36),27:('27 / FLOWERING GARDEN - ROOM TO BREATHE',(19,-.5,8),(18,-1.5,7),(10.9,-6.8,.8),32),28:('28 / FLOWING WALKS AND GARDEN COURT',(22,27,17),(20,25,15),(0,13,0),35)}
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
for number,(title,a,b,target,lens) in newshots.items():
c=chapters[number-1];c['title']=title
for f,p in [(c['start'],a),(c['end'],b)]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=f);cam.keyframe_insert('rotation_euler',frame=f);cam.data.lens=lens;cam.data.keyframe_insert('lens',frame=f)
for ob in fonts:
if ob.data.body.startswith(str(number).zfill(2)+' /'):
ob.data.body=title;half=36/lens;ob.location=(-half*.91,half*9/16*.84,-2)
s.timeline_markers.clear()
for c in chapters:s.timeline_markers.new(c['title'],frame=c['start'])
s['Current design']='Aligned garden homestead: main house and garage share a north-south axis; meditation wing and future east pavilion balance the oval garden. Smooth gravel routes, a curved covered timber breezeway and layered flowering beds replace the isolated table-tennis deck.'
s['Landscape design basis']='Preserve north-facing outlook and open air movement; low planting close to buildings, repeated beds and a simple hierarchy of shared court, pedestrian paths and vehicle arrival. Concept terrain remains unsurveyed.'
s['PLAY THE TOUR']='Hover over the 3D view and press Space. 30 chapters, 2 min 30 sec. Shift+Left Arrow restarts.'
text=bpy.data.texts.get('START HERE - PRESS PLAY')
if text:text.clear();text.write(s['PLAY THE TOUR']+'\n\n'+s['Current design'])
layout={'version':4,'main':[0,0,12,6],'parents':[-11.8,-4.9,8,5],'meditation':[-11.8,12,4.5,4.8],'future':[[11.8,12,4.5,4.8],[0,24,4.5,4.8]],'garage':[0,-19,8,7],'training':[-9.4,-19,4.8,5.4],'solar':[9.4,-19,7.3,5.2],'sauna':[-23,4.2],'outdoor_table':[10,3.5,6,5.8],'court':[0,13.2,8.5,5.3],'paths':paths,'beds':beds,'table_tennis':False,'future_basketball':[13,-36,10,10]}
json.dump(layout,open(OUT+'/garden-flow-layout.json','w'),indent=2);json.dump(chapters,open(OUT+'/tour-chapters-garden-flow.json','w'),indent=2)
report={'deleted_objects':len(removed),'moved_objects':len(moved),'flower_beds':len(beds),'planted_clumps':len(flower_centers),'flowering_shrubs':8,'smooth_routes':len(paths),'table_tennis_removed':not any('table tennis' in o.name.lower() or o.name.startswith('PLAY DECK') for o in s.objects),'future_pavilions':2,'tour_chapters':len(chapters)}
assert report['table_tennis_removed']
json.dump(report,open(OUT+'/garden-flow-checks.json','w'),indent=2)
s.camera=cam;s.frame_set(1);s.render.resolution_percentage=100
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-garden-flow-tour.blend')
for o in fonts:o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=32;s.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
for d in prefs.devices:d.use=d.type=='METAL'
s.cycles.device='GPU'
for filename,frame in [('garden-flow-overview.png',45),('garden-court.png',2670),('flowering-kitchen-garden.png',3160),('garden-flow-training.png',2920),('garden-flow-dining.png',3035)]:
s.frame_set(frame);s.render.filepath=OUT+'/'+filename;bpy.ops.render.render(write_still=True)
print('GARDEN_FLOW_READY '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,269 @@
import bpy, math, os, json, random, bmesh
from mathutils import Vector
from math import sin,cos,pi
random.seed(602)
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
# Keep one room as a retreat; the other two become undeveloped shadow footprints.
med=bpy.data.collections['23 FUTURE - Child 1 private room']; med.name='23 NOW - Quiet meditation room'; med['Purpose']='Simple meditation retreat now; adaptable room in future.'
for name in ['24 FUTURE - Child 2 private room','25 FUTURE - Child 3 private room','26 Children courtyard and covered links']:
c=bpy.data.collections.get(name)
if c:
for o in list(c.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.data.collections.remove(c)
for o in list(med.objects):
if any(t in o.name for t in ['single bed','mattress','blanket','pillow','desk','stool','wardrobe','pinboard','display shelf']): bpy.data.objects.remove(o,do_unlink=True)
elif o.type=='FONT': o.data.body='QUIET / MEDITATION'; o.name='Meditation room entry sign'
else: o.name=o.name.replace('Child 1','Meditation').replace('Child room','Meditation room')
# Remove roof-junction leftovers from the two deleted rooms, which were stored separately.
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
if o.name.startswith('Child room'):
if center(o).x>-6: bpy.data.objects.remove(o,do_unlink=True)
else: o.name=o.name.replace('Child room','Meditation room')
COL={'med':med}
for key,name in [('future','24 LATER - Shadow spaces only'),('road','27 Gravel access road and arrival'),('garage','28 Garage and workbench'),('car','29 Car and bicycles'),('cutaway','30 VIEW - Transparent building sides')]:
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
active='med'; src=open(os.path.join(OUT,'../samford-modular/build_scene.py')).read(); exec(src[src.index('def mesh('):src.index('timber=textured(')])
wood=bpy.data.materials['Unpainted hardwood - long X grain']; woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain']; steel=bpy.data.materials['Reusable dark steel bolts and shoes']; stone=bpy.data.materials['Split sandstone facing - mineral grain']; roofmat=bpy.data.materials['Pale Colorbond - powder coated steel']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; weather=bpy.data.materials['Naturally silvered hardwood - horizontal X']; linen=bpy.data.materials['Undyed flax linen']; gravel=bpy.data.materials['Loose local gravel - fine stones']; F=1.12
zafu=textured('Meditation cushion charcoal linen',(.085,.08,.063),(.19,.17,.13),(100,100,100),.95,.003)
tatami=textured('Woven natural meditation mat',(.33,.28,.17),(.54,.47,.31),(120,8,120),.94,.004)
wax=mat('Beeswax candle',(.70,.50,.22),.63); flame=mat('Small warm candle flame',(.95,.43,.08),.5); p=flame.node_tree.nodes['Principled BSDF']; p.inputs['Emission Color'].default_value=(1,.34,.04,1); p.inputs['Emission Strength'].default_value=5
box('Simple woven tatami mat',(-11,12,F+.025),(2.05,2.2,.05),tatami,.035)
box('Meditation square zabuton',(-11,12.05,F+.075),(.95,.95,.10),linen,.10)
rod('ROUND MEDITATION CUSHION',(-11,12.05,F+.12),(-11,12.05,F+.28),.34,zafu,sides=64)
box('Low stone candle shelf',(-11,10.36,F+.29),(1.35,.38,.12),stone,.022)
for x,h in [(-11.42,.16),(-11,.22),(-10.58,.13)]:
rod('Stone candle holder',(x,10.36,F+.35),(x,10.36,F+.39),.10,stone,sides=24)
rod('Meditation candle',(x,10.36,F+.39),(x,10.36,F+.39+h),.041,wax,sides=24)
rod('Meditation candle wick',(x,10.36,F+.39+h),(x,10.36,F+.415+h),.003,steel,sides=8)
rod('Small candle flame',(x,10.36,F+.40+h),(x,10.36,F+.46+h),.014,flame,r2=.001,sides=12)
d=bpy.data.lights.new('Soft candle glow','POINT'); d.energy=1.5; d.color=(1,.39,.10); d.shadow_soft_size=.06; ob=bpy.data.objects.new(d.name,d); COL[active].objects.link(ob); ob.location=(x,10.36,F+.44+h)
box('Meditation low timber bench',(-12.6,11.9,F+.37),(.45,1.45,.065),wood,.02)
for y in [11.34,12.46]: box('Meditation bench leg',(-12.6,y,F+.18),(.36,.05,.36),wood)
# A minimal shaded walk goes only to the actual meditation room.
for cx,cy,w,dep in [(-3,7.2,1.2,3.6),(-7,9,8,1.2)]:
box('Meditation covered walk deck',(cx,cy,F-.07),(w,dep,.14),wood,.008)
box('Meditation covered walk roof',(cx,cy,F+2.55),(w+.14,dep+.14,.055),roofmat,.008)
for x,y in [(cx-w/2+.1,cy-dep/2+.1),(cx+w/2-.1,cy+dep/2-.1)]:
g=ground(x,y); box('Meditation walk timber post',(x,y,(g+F+2.52)/2),(.085,.085,F+2.52-g),woodz,.006)
def ghost(name,color,opacity):
m=mat(name,color,.8); ns=m.node_tree.nodes; ls=m.node_tree.links; p=ns.get('Principled BSDF'); out=ns.get('Material Output'); tr=ns.new('ShaderNodeBsdfTransparent'); mix=ns.new('ShaderNodeMixShader'); mix.inputs[0].default_value=opacity; ls.new(tr.outputs[0],mix.inputs[1]); ls.new(p.outputs[0],mix.inputs[2]); ls.new(mix.outputs[0],out.inputs['Surface']); m.diffuse_color=(*color,opacity); return m
active='future'; gm=ghost('Future shadow - faint sage outline',(.49,.59,.50),.32); shade=ghost('Future shadow - translucent footprint',(.20,.26,.21),.10)
for idx,(cx,cy) in enumerate([(-3,17),(7,15)]):
xs,xe=cx-2.25,cx+2.25; ys,yn=cy-2.4,cy+2.4
for x in [xs,xe]:
for y,h in [(ys,2.75),(yn,3.1)]: beam('Future space %d | shadow edge'%(idx+1),(x,y,F),(x,y,F+h),.028,.028,gm)
beam('Future shadow roof slope',(x,ys,F+2.75),(x,yn,F+3.1),.028,.028,gm)
for y,h in [(ys,2.75),(yn,3.1)]:
beam('Future shadow roof line',(xs,y,F+h),(xe,y,F+h),.028,.028,gm)
beam('Future shadow floor line',(xs,y,F),(xe,y,F),.028,.028,gm)
for x in [xs,xe]: beam('Future shadow floor side',(x,ys,F),(x,yn,F),.028,.028,gm)
o=box('Future space %d | ground shadow'%(idx+1),(cx,cy,ground(cx,cy)+.03),(4.5,4.8,.01),shade); o['Status']='Unbuilt reserve: no furniture, solid walls or occupied room.'
# Road enters from the southeast and approaches a turning apron in front of the garage.
active='road'; roadpoints=[(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)]
def road(name,points,width):
vs=[]; fs=[]
for aa,bb in zip(points,points[1:]):
a,b=Vector(aa),Vector(bb); delta=b-a; perp=Vector((-delta.y,delta.x)).normalized()*width/2; n=max(3,int(delta.length/.45))
start=len(vs)
for i in range(n):
p=a+delta*i/(n-1)
for q in [p-perp,p+perp]: vs.append((q.x,q.y,ground(q.x,q.y)+.065))
for i in range(n-1): k=start+i*2; fs.append((k,k+1,k+3,k+2))
return mesh(name,vs,fs,gravel)
road('VEHICLE ACCESS - 4m gravel driveway',roadpoints,4)
# Gravel apron draped over local terrain.
vs=[]; fs=[]
for j in range(9):
for i in range(17):
x=-7+i*.75; y=-15.5+j*.75; vs.append((x,y,ground(x,y)+.07))
for j in range(8):
for i in range(16): k=j*17+i; fs.append((k,k+1,k+18,k+17))
mesh('Gravel turning and garage arrival apron',vs,fs,gravel)
road('Garage to house pedestrian path',[(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],1.1)
for x,y in [(34,-37),(29,-32),(22,-27),(6,-24),(5,-18),(3,-10)]:
g=ground(x,y); box('Simple timber driveway edge marker',(x+2.35,y,g+.37),(.09,.09,.74),woodz,.01)
# Detached garage: one car bay, workshop bench and a clear cycle-storage zone.
active='garage'; gx,gy=-3,-19; GF=.62; w,dep=8,7; xs,xe=gx-4,gx+4; ys,yn=gy-3.5,gy+3.5
slab=box('GARAGE - 8m x 7m concrete floor',(gx,gy,GF-.11),(w,dep,.22),bpy.data.materials['Warm oxide polished concrete - fine aggregate'],.018)
for x in [xs+.15,xe-.15]:
for y in [ys+.15,yn-.15]:
g=ground(x,y); box('Garage concrete foundation pier',(x,y,(g+GF-.2)/2),(.40,.40,max(.08,GF-.2-g)),stone,.015)
box('Garage back clay wall',(gx,ys,GF+1.44),(w,.17,2.88),clay)
box('Garage west weatherboard wall',(xs,gy,GF+1.51),(.17,dep,3.02),weather)
box('Garage east workshop wall',(xe,gy,GF+1.51),(.17,dep,3.02),clay)
# The north viewing facade retains a closed-door design underneath the transparent presentation.
box('Garage north front panel',(-5.0,yn,GF+1.43),(3.7,.15,2.86),weather)
box('Garage north pedestrian door',(.24,yn,GF+1.16),(1.16,.07,2.32),wood,.009)
box('Garage north narrow infill',(-.66,yn,GF+1.43),(.61,.15,2.86),weather)
box('Garage north upper header',(gx,yn,GF+3.03),(w,.17,.36),wood)
box('Garage roller door concept',(-2.14,yn,GF+1.30),(1.9,.09,2.6),roofmat,.008)
# The car bay uses the broad west section: transparent front panels expose its parked car.
for x in [xs,xe,-2.8]:
for y,h in [(ys,2.95),(yn,3.3)]: box('Garage exposed timber post',(x,y,GF+h/2),(.14,.14,h),woodz,.005)
for i in range(12):
x=xs+i*w/11; beam('Garage exposed rafter',(x,ys-.30,GF+2.94),(x,yn+.30,GF+3.33),.07,.20,wood)
ro=box('Garage pale mono-pitch roof',(gx,gy,GF+3.22),(8.7,7.7,.075),roofmat,.01); ro.rotation_euler.x=math.atan(.35/7)
box('Garage rear roof gutter',(gx,ys-.40,GF+3.03),(8.7,.14,.10),roofmat,.006)
# Entrance ramp blends the vehicle bay floor down to the apron.
vs=[(x,y,z) for x in [xs+.2,gx+.3] for y,z in [(yn,GF),(yn+2.7,ground(x,yn+2.7)+.07)]]
mesh('Garage shallow entry ramp',vs,[(0,2,3,1)],gravel)
benchx,benchy=-.47,-21.5
box('GARAGE WORKSTATION - timber workbench',(benchx,benchy,GF+.92),(2.50,.76,.09),wood,.015)
for x in [benchx-1.07,benchx+1.07]:
for y in [benchy-.28,benchy+.28]: box('Workbench steel leg',(x,y,GF+.44),(.055,.055,.88),steel,.005)
box('Workbench lower shelf',(benchx,benchy,GF+.22),(2.4,.66,.045),wood)
box('Workshop toolboard',(benchx,ys+.12,GF+1.85),(2.5,.07,.98),wood,.008)
for i in range(8):
x=benchx-1.05+i*.30; rod('Workbench tool hook',(x,ys+.17,GF+1.9),(x,ys+.25,GF+1.9),.009,steel)
box('Workshop hanging tool handle',(x,ys+.23,GF+1.68),(.03,.03,.27),woodz,.005)
box('Workshop tool head',(x,ys+.23,GF+1.89),(.10,.045,.06),steel,.008)
box('Workbench small vice',(benchx-.85,benchy+.34,GF+1.04),(.25,.20,.17),steel,.02)
rod('Vice turning handle',(benchx-1.00,benchy+.48,GF+1.03),(benchx-.69,benchy+.48,GF+1.03),.012,steel)
black=mat('Garage rubber and dark tools',(.025,.03,.028),.78); alu=mat('Garage brushed aluminium',(.40,.43,.43),.30,.8)
box('Workbench compact display',(benchx+.5,benchy-.19,GF+1.22),(.56,.04,.34),black,.015)
box('Workbench keyboard',(benchx+.5,benchy+.12,GF+.98),(.34,.12,.018),black,.007)
box('Workbench stool',(benchx,benchy+1.1,GF+.51),(.42,.42,.06),wood,.025)
for x in [benchx-.16,benchx+.16]:
for y in [benchy+.94,benchy+1.26]: box('Workbench stool leg',(x,y,GF+.24),(.035,.035,.48),steel)
light=bpy.data.lights.new('Garage warm work light','AREA'); light.energy=320; light.size=3.4; light.color=(1,.85,.66); ob=bpy.data.objects.new(light.name,light); COL[active].objects.link(ob); ob.location=(-2,-19,GF+2.7)
# One generic family wagon, parked nose toward the transparent front.
active='car'; paint=mat('Family wagon muted green',(.20,.28,.25),.32,.4); autoglass=mat('Vehicle tinted glass',(.035,.07,.085),.20,.3); lamps=mat('Vehicle headlamp lens',(.70,.76,.73),.22)
cx,cy=-4.7,-19.1
box('ONE FAMILY CAR - wagon body',(cx,cy,GF+.70),(1.86,4.35,.56),paint,.16)
box('Car lower sill',(cx,cy,GF+.46),(1.77,4.12,.22),black,.08)
vs=[(cx+x,cy+y,GF+z) for x,y,z in [(-.81,-1.25,.96),(.81,-1.25,.96),(.81,1.08,.96),(-.81,1.08,.96),(-.69,-.97,1.65),(.69,-.97,1.65),(.69,.64,1.65),(-.69,.64,1.65)]]
mesh('Family wagon glazed cabin',vs,[(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7),(4,5,6,7)],autoglass)
box('Car roof',(cx,cy-.16,GF+1.67),(1.43,1.65,.075),paint,.05)
for x in [cx-.87,cx+.87]:
for y in [cy-1.40,cy+1.40]:
rod('Car rubber tyre',(x-.11,y,GF+.36),(x+.11,y,GF+.36),.35,black,sides=48)
rod('Car alloy wheel',(x-.12,y,GF+.36),(x+.12,y,GF+.36),.23,alu,sides=32)
box('Car side mirror',(x+(.13 if x>cx else -.13),cy+.55,GF+1.08),(.20,.24,.12),paint,.05)
box('Car door handle',(x+(.07 if x>cx else -.07),cy,GF+.93),(.025,.17,.027),alu,.01)
for x in [cx-.61,cx+.61]: box('Car front headlamp',(x,cy+2.14,GF+.82),(.45,.04,.15),lamps,.045)
box('Car front bumper',(cx,cy+2.18,GF+.50),(1.68,.05,.15),black,.05)
box('Car blank number plate',(cx,cy+2.22,GF+.65),(.42,.018,.10),lamps,.01)
# Two bicycles in a three-position floor rack beside the car, clear of the bench.
bikemat=[mat('Bicycle ochre frame',(.53,.31,.10),.4,.45),mat('Bicycle blue green frame',(.10,.30,.31),.4,.45)]
def hoop(name,x,y,z,r,tube,material):
n=56; vs=[]; fs=[]
for i in range(n):
a=2*pi*i/n
for j in range(8):
b=2*pi*j/8; rr=r+tube*cos(b); vs.append((x+tube*sin(b),y+rr*cos(a),z+rr*sin(a)))
for i in range(n):
for j in range(8): fs.append((i*8+j,i*8+(j+1)%8,((i+1)%n)*8+(j+1)%8,((i+1)%n)*8+j))
o=mesh(name,vs,fs,material)
for p in o.data.polygons:p.use_smooth=True
for idx,bx in enumerate([-.65,.25]):
by=-18.0; z=GF+.36; m=bikemat[idx]; rear=Vector((bx,by-.66,z)); front=Vector((bx,by+.66,z)); crank=Vector((bx,by-.04,z+.05)); saddle=Vector((bx,by-.27,z+.63)); head=Vector((bx,by+.43,z+.59))
for wheel in [rear,front]:
hoop('Bicycle %d tyre'%(idx+1),wheel.x,wheel.y,wheel.z,.34,.022,black)
hoop('Bicycle aluminium rim',wheel.x,wheel.y,wheel.z,.302,.010,alu)
for i in range(12):
a=2*pi*i/12; rod('Bicycle spoke',wheel,wheel+Vector((0,.302*cos(a),.302*sin(a))),.003,alu,sides=5)
for a,b in [(rear,saddle),(saddle,crank),(crank,rear),(saddle,head),(crank,head),(head,front)]: rod('Bicycle frame tube',a,b,.022,m,sides=12)
box('Bicycle saddle',(bx,saddle.y,z+.69),(.19,.27,.05),black,.04)
rod('Bicycle handlebar',(bx-.26,head.y+.04,z+.81),(bx+.26,head.y+.04,z+.81),.014,alu,sides=12)
rod('Bicycle stem',head,(bx,head.y+.04,z+.81),.015,alu,sides=12)
rod('Bicycle crank',(bx-.15,crank.y,crank.z),(bx+.15,crank.y,crank.z),.014,steel,sides=12)
for x in [-.65,.25,-1.55]:
for xx in [x-.075,x+.075]: rod('BIKE RACK wheel guide',(xx,-17.47,GF+.03),(xx,-16.97,GF+.03),.025,steel,sides=12)
rod('Bike rack upright',(x,-16.98,GF+.03),(x,-16.98,GF+.5),.025,steel,sides=12)
box('Bike rack shared base',(-.65,-17.22,GF+.03),(2.45,.58,.055),steel,.01)
# Non-destructive presentation cutaways: retain wall geometry but use near-transparent skin materials.
cutawaycache={}; cutobjects=[]
def transparent_side(o):
if o.type!='MESH': return
original=[]
for slot in o.material_slots:
m=slot.material
if not m: continue
original.append(m.name)
if m.name not in cutawaycache:
col=tuple(m.diffuse_color[:3]); cutawaycache[m.name]=ghost('VIEW transparent / '+m.name,col,.055)
slot.material=cutawaycache[m.name]
o['Original wall materials']=json.dumps(original); o['Presentation only']='Transparent viewing facade. Original solid-wall material names preserved here.'
# Hide filled wall faces in fast solid viewport, leaving structural posts and opening frames visible.
o.hide_set(True); cutobjects.append(o.name)
for cc in list(o.users_collection): cc.objects.unlink(o)
COL['cutaway'].objects.link(o)
for name in ['01 Living pavilion 12 x 6 m','02 Sleeping pavilion 8 x 5 m','23 NOW - Quiet meditation room','16 Oval Finnish sauna','06 Kitchen garden','28 Garage and workbench','20 Spacious site connections']:
coll=bpy.data.collections.get(name)
if not coll: continue
for o in list(coll.objects):
n=o.name; p=center(o); use=False
if name.startswith('01'): use=n.startswith('Sliding glass pane')
elif name.startswith('02'): use=n.startswith(('Bedroom north wall','Bedroom north lintel','Sliding glass pane'))
elif name.startswith('23'): use=n.startswith(('Meditation room north clay wall','Meditation room north window infill','Meditation room north openable glazing'))
elif name.startswith('16'): use=n.startswith(('Sauna front timber cheek','Sauna entrance oval infill','Sauna continuous tempered-glass','Sauna outward-opening'))
elif name.startswith('06'): use=('Greenhouse end glazing' in n and p.y>-6) or ('Greenhouse side glazing' in n and p.x>24)
elif name.startswith('28'): use=n.startswith(('Garage north front panel','Garage north pedestrian door','Garage north narrow infill','Garage roller door concept'))
elif name.startswith('20'): use=n.startswith('Corridor north wall') or (n.startswith('Meditation room north wall roof junction'))
if use: transparent_side(o)
# Fix all newly created solid normals, including stock helpers inherited from the first model.
for coll in COL.values():
for o in coll.objects:
if o.type=='MESH':
bm=bmesh.new(); bm.from_mesh(o.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(o.data); bm.free()
# Replace obsolete children-room chapters with the meditation retreat, garage and future shadows.
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data; tour=bpy.data.collections['22 PLAY - guided camera tour']; caption=bpy.data.materials['Tour captions ivory']
chapters=json.load(open(os.path.join(OUT,'../samford-spacious-tour/tour-chapters.json')))[:17]
for o in list(tour.objects):
if o.type=='FONT' and int(o.name[:2])>=18: bpy.data.objects.remove(o,do_unlink=True)
for m in list(s.timeline_markers):
if m.frame>=2041: s.timeline_markers.remove(m)
newshots={
17:('18 / QUIET MEDITATION ROOM',(-8.4,16.5,3.8),(-9,15.8,3.4),(-11,11.7,1.65),(-11,11.5,1.65),32),
18:('19 / CUSHION, CANDLES & STILLNESS',(-9.6,13.5,2.7),(-10,13.1,2.55),(-11,11.3,1.45),(-11,11.0,1.45),27),
19:('20 / GRAVEL ROAD & GARAGE',(17,-4,10),(13,-7,8),(-3,-18,1.5),(-3,-18,1.5),36),
20:('21 / CAR, WORKBENCH & BIKE RACKS',(-.5,-11,4.0),(-1.4,-12,3.6),(-2.3,-19,1.5),(-2.3,-19.4,1.5),30),
21:('22 / FUTURE SPACES - BUILD LATER',(19,28,13),(16,25,11),(2,16,1.5),(2,16,1.5),37),
22:('23 / A HOME THAT CAN CHANGE',(44,64,47),(49,69,50),(-3,-4,1),(-3,-4,1),40),
}
for i,(title,a,b,ta,tb,lens) in newshots.items():
start=i*120+1; end=(i+1)*120
for f,pos,target in [(start,a,ta),(end,b,tb)]:
cam.location=pos; cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=f); cam.keyframe_insert('rotation_euler',frame=f); data.lens=lens; data.keyframe_insert('lens',frame=f)
s.timeline_markers.new(title,frame=start)
cu=bpy.data.curves.new(title,'FONT'); cu.body=title; cu.size=.036; ob=bpy.data.objects.new(title,cu); tour.objects.link(ob); ob.parent=cam; half=36/lens; ob.location=(-half*.91,half*9/16*.84,-2); ob.show_in_front=True; cu.materials.append(caption)
for f,val in [(0,0),(start-1,0),(start,1),(end,1),(end+1,0)]: ob.scale=(val,)*3; ob.keyframe_insert('scale',frame=f)
chapters.append({'title':title,'start':start,'end':end,'seconds':5})
def curves(action):
if hasattr(action,'fcurves'): return action.fcurves
result=[]
for layer in action.layers:
for strip in layer.strips:
for slot in action.slots:
bag=strip.channelbag(slot)
if bag: result.extend(bag.fcurves)
return result
for owner in [cam,data]:
for fc in curves(owner.animation_data.action):
for k in fc.keyframe_points:k.interpolation='LINEAR'
for o in tour.objects:
if o.type=='FONT':
for fc in curves(o.animation_data.action):
for k in fc.keyframe_points:k.interpolation='CONSTANT'
s.frame_end=2760; s.frame_set(1); s.camera=cam; s.sync_mode='FRAME_DROP'
s['PLAY THE TOUR']='Space over 3D view or Timeline Play. Shift+Left Arrow restarts. 115 seconds / 23 chapters / 24 fps.'
s['Current design']='One meditation retreat with cushion and candles; two unbuilt shadow spaces. Detached 8x7m garage with car, workbench and bicycle racks. Southeast gravel driveway. Transparent viewing facades are presentation-only.'
s['Future additions']='Two faint unbuilt shadow rooms replace solid child rooms. Meditation cabin can be adapted later.'
s['Transparent walls']='Collection 30 stores near-transparent wall skins. Filled faces hidden only in solid viewport. Structural frames stay visible. Original materials recorded on each object.'
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Current design']+'\n\n'+s['Transparent walls'])
names=[o.name for o in s.objects]
report={'future_shadow_spaces':2,'meditation_cushions':sum(n.startswith('ROUND MEDITATION CUSHION') for n in names),'candles':sum(n.startswith('Meditation candle') and 'wick' not in n for n in names),'garage_size_metres':[8,7],'cars':sum(n.startswith('ONE FAMILY CAR') for n in names),'bicycles':2,'rack_positions':3,'rainwater_tanks':sum(n.startswith('2000 mm') for n in names),'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),'garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),'tour_seconds':115,'chapters':len(chapters),'transparent_wall_objects':len(cutobjects)}
assert report['meditation_cushions']==1 and report['cars']==1 and report['rainwater_tanks']==2 and report['solar_panels']==30 and report['garden_beds']==6 and report['transparent_wall_objects']>20
json.dump(report,open(os.path.join(OUT,'design-checks.json'),'w'),indent=2); json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
s.render.engine='CYCLES'; s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
# Fast chapter checks are separate from the saved model's render configuration.
for o in COL['cutaway'].objects:o.hide_render=True
for o in s.objects:
if o.name=='Subtle atmosphere over hillside':o.hide_render=True
s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.light='STUDIO'; s.display.shading.color_type='MATERIAL'; s.display.shading.show_shadows=True; s.display.shading.show_cavity=True
for i in [0,6,9,17,18,19,20,21,22]:
s.frame_set(i*120+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
print('RETREAT_GARAGE_DRAFT_COMPLETE',flush=True)

View File

@@ -0,0 +1,139 @@
import bpy,math,random,os,json,bmesh
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene;random.seed(41)
COL={'car':bpy.data.collections['29 Car and bicycles'],'garage':bpy.data.collections['28 Garage and workbench']};active='car'
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
# Restore outward cube winding for newly added geometry.
cubefs=[tuple(reversed(f)) for f in cubefs]
for o in list(COL['car'].objects):
if o.name.startswith(('Car ','Family wagon','ONE FAMILY CAR')):bpy.data.objects.remove(o,do_unlink=True)
paint=mat('Tesla pearl white multi-coat',(.82,.84,.81),.23,.25);paint.node_tree.nodes['Principled BSDF'].inputs['Coat Weight'].default_value=.55
glass=mat('Tesla smoked panoramic glass',(.026,.043,.055),.14,.35)
rubber=mat('Tesla tyre rubber',(.018,.020,.021),.82)
black=mat('Tesla satin black trim',(.025,.029,.031),.4,.4)
silver=mat('Tesla machined wheel accents',(.52,.57,.59),.22,.9)
led=mat('Tesla LED white',(.8,.89,.91),.17);p=led.node_tree.nodes['Principled BSDF'];p.inputs['Emission Color'].default_value=(.8,.9,1,1);p.inputs['Emission Strength'].default_value=.8
red=mat('Tesla rear light red',(.42,.009,.012),.25)
cx,cy,GF=-4.7,-19.1,.62
def world(p):return (cx+p[0],cy+p[1],GF+p[2])
def path(name,points,r,material):
cu=bpy.data.curves.new(name,'CURVE');cu.dimensions='3D';cu.resolution_u=8;cu.bevel_depth=r;cu.bevel_resolution=2
sp=cu.splines.new('BEZIER');sp.bezier_points.add(len(points)-1)
for b,p in zip(sp.bezier_points,points):b.co=p;b.handle_left_type='AUTO';b.handle_right_type='AUTO'
ob=bpy.data.objects.new(name,cu);COL[active].objects.link(ob);cu.materials.append(material)
return ob
def apply_smooth(ob,level=1):
bm=bmesh.new();bm.from_mesh(ob.data);bmesh.ops.recalc_face_normals(bm,faces=bm.faces);bm.to_mesh(ob.data);bm.free()
bpy.context.view_layer.objects.active=ob;ob.select_set(True)
if level:
m=ob.modifiers.new('Baked flowing body surface','SUBSURF');m.levels=level;bpy.ops.object.modifier_apply(modifier=m.name)
for p in ob.data.polygons:p.use_smooth=True
ob.select_set(False)
# 2020-24 Model Y inspired body, 4.75m long, with open wheel arches.
ys=[-2.375,-2.34,-2.22,-2.02,-1.85,-1.70,-1.55,-1.445,-1.33,-1.17,-1.03,-.88,-.6,0,.55,.86,1.03,1.18,1.32,1.445,1.57,1.72,1.86,2.05,2.25,2.34,2.375]
vs=[];fs=[]
for y in ys:
w=.965-.14*(abs(y)/2.375)**6
top=.99-.23*max(0,(y-.8)/1.575)+.05*max(0,(-y-1.3)/1.075)
arch=max([.28]+[.37+math.sqrt(max(0,.405**2-(y-wy)**2)) for wy in [-1.445,1.445] if abs(y-wy)<.405])
ring=[(-w*.86,.25),(-w,arch),(-w,top-.13),(-w*.91,top),(-w*.52,top+.035),(0,top+.045),(w*.52,top+.035),(w*.91,top),(w,top-.13),(w,arch),(w*.86,.25),(0,.23)]
vs.extend(world((x,y,z)) for x,z in ring)
for j in range(len(ys)-1):
for i in range(12):a=j*12+i;b=j*12+(i+1)%12;fs.append((a,b,b+12,a+12))
fs.extend([tuple(reversed(range(12))),tuple(range((len(ys)-1)*12,len(ys)*12))])
body=mesh('TESLA MODEL Y - pearl white flowing body',vs,fs,paint);apply_smooth(body,2)
# Arched glass roof, windscreen and rear hatch form one continuous dark canopy.
sections=[(-2.04,.68,1.02,1.10),(-1.8,.73,1.02,1.28),(-1.28,.75,1.03,1.53),(-.8,.74,1.04,1.62),(-.22,.74,1.04,1.64),(.32,.74,1.03,1.60),(.75,.78,1.00,1.40),(1.12,.81,.99,1.06)]
vs=[];fs=[]
for y,w,b,t in sections:
for x,z in [(-w,b),(-w*.92,b+(t-b)*.7),(-w*.73,t-.025),(0,t),(w*.73,t-.025),(w*.92,b+(t-b)*.7),(w,b)]:vs.append(world((x,y,z)))
for j in range(len(sections)-1):
for i in range(6):a=j*7+i;fs.append((a,a+1,a+8,a+7))
canopy=mesh('Tesla sweeping panoramic glass roof and windows',vs,fs,glass);apply_smooth(canopy,2)
for side in [-1,1]:
path('Tesla continuous window surround',[world((side*w,y,b+.025)) for y,w,b,t in sections],.022,black)
path('Tesla roof shoulder pillar',[world((side*w*.79,y,t-.075)) for y,w,b,t in sections[1:-1]],.023,paint)
for y,top in [(.69,1.40),(-.32,1.59),(-1.3,1.45)]:
path('Tesla window pillar',[world((side*.81,y,1.025)),world((side*.65,y,top))],.025,black if y<.1 else paint)
for y in [-.65,.55]:box('Tesla flush black door handle',world((side*.963,y,.925)),(.016,.17,.025),black,.006)
path('Tesla door shut line',[world((side*.963,-.30,.36)),world((side*.965,-.32,.75)),world((side*.91,-.34,1.01))],.0035,black)
box('Tesla side mirror',world((side*1.02,.70,1.10)),(.24,.18,.10),black,.045)
for wy in [-1.445,1.445]:
x=side*.935
tyre=rod('Tesla 19 inch tyre',world((x-.12,wy,.36)),world((x+.12,wy,.36)),.355,rubber,sides=64)
outer=side*1.061
rod('Tesla dark aero wheel',world((outer,wy,.36)),world((outer+side*.008,wy,.36)),.248,black,sides=48)
rod('Tesla wheel centre',world((outer+side*.01,wy,.36)),world((outer+side*.019,wy,.36)),.061,silver,sides=24)
for k in range(5):
a=k*2*pi/5
path('Tesla turbine spoke',[world((outer+side*.011,wy+.07*cos(a),.36+.07*sin(a))),world((outer+side*.014,wy+.22*cos(a+.22),.36+.22*sin(a+.22)))],.022,silver)
path('Tesla narrow swept headlamp',[world((side*.47,2.30,.79)),world((side*.69,2.23,.82)),world((side*.82,2.08,.84))],.029,led)
path('Tesla red rear lamp',[world((side*.41,-2.34,1.00)),world((side*.67,-2.33,.99)),world((side*.83,-2.22,.94))],.028,red)
box('Tesla lower front air intake',world((0,2.351,.40)),(1.24,.026,.09),black,.035)
# Recognisable Tesla T emblem on the nose; no imported heavy vehicle asset.
path('Tesla T badge upper arc',[world((-.08,2.17,.828)),world((0,2.20,.824)),world((.08,2.17,.828))],.006,black)
path('Tesla T badge stem',[world((0,2.19,.826)),world((0,2.29,.812))],.006,black)
def label(name,body,pos,rot,size,material):
cu=bpy.data.curves.new(name,'FONT');cu.body=body;cu.size=size;cu.align_x='CENTER';cu.extrude=.0005
ob=bpy.data.objects.new(name,cu);COL[active].objects.link(ob);ob.location=pos;ob.rotation_euler=rot;cu.materials.append(material);return ob
box('Tesla front number plate',world((0,2.382,.60)),(.41,.018,.105),paint,.012)
label('Tesla MODEL Y plate','MODEL Y',world((0,2.394,.573)),(pi/2,0,pi),.061,black)
label('Tesla rear lettering','T E S L A',world((0,-2.38,.92)),(pi/2,0,0),.078,silver)
active='garage'
wood=bpy.data.materials['Unpainted hardwood - long X grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes']
solar=mat('Garage solar blue black cells',(.023,.065,.098),.23,.65);green=mat('Solar charge indicator',(.1,.65,.3),.3)
green.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.1,1,.3,1);green.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=1.3
box('SOLAR CHARGING - timber backboard',(-6.88,-20.8,GF+1.45),(.07,.52,.89),wood,.025)
box('Tesla wall connector',(-6.79,-20.8,GF+1.45),(.16,.24,.47),paint,.075)
box('Tesla charger black face',(-6.70,-20.8,GF+1.46),(.016,.20,.37),black,.035)
box('Solar charging green status',(-6.685,-20.8,GF+1.62),(.009,.13,.014),green,.005)
path('EV cable to left rear charge port',[(-6.73,-20.8,GF+1.20),(-6.59,-20.8,GF+.60),(-6.34,-20.8,GF+.20),(-5.96,-20.92,GF+.24),(-5.71,-21.08,GF+.76),(-5.66,-21.08,GF+.93)],.014,black)
box('Tesla plugged charge connector',(-5.67,-21.08,GF+.94),(.09,.07,.07),black,.022)
for x in [-5.9,-4.0,-2.1,-.2]:
for y in [-20.9,-18.5]:
z=GF+3.22+.05*(y+19)+.105
for name,dims,offset,material in [('Garage solar aluminium frame',(1.78,2.14,.05),0,silver),('Garage photovoltaic module',(1.70,2.06,.018),.034,solar)]:
ob=box(name,(x,y,z+offset),dims,material,.009);ob.rotation_euler.x=math.atan(.05)
for i in range(1,6):
ob=box('Solar cell division',(x-.85+i*1.7/6,y,z+.046),(.005,2.02,.004),silver);ob.rotation_euler.x=math.atan(.05)
for j in range(1,10):
yy=y-1.03+j*2.06/10;box('Solar cell row',(x,yy,z+.046+.05*(yy-y)),(1.69,.005,.004),silver)
# A planted trellis softens the workshop's east wall without narrowing car access.
soil=bpy.data.materials['Damp brown garden soil'];leaves=[bpy.data.materials['Leaf '+str(i)] for i in range(5)]
for x in [1.23,2.03]:box('Garage edible planter timber side',(x,-19,.79),(.055,3.6,.43),wood,.015)
for y in [-20.8,-17.2]:box('Garage edible planter end',(1.63,y,.79),(.85,.055,.43),wood,.015)
box('Garage edible planter soil',(1.63,-19,.94),(.75,3.5,.06),soil)
for y in [-20.7,-19.8,-18.9,-18,-17.3]:box('Garage climbing garden upright',(1.14,y,1.88),(.04,.04,2.35),wood)
for z in [1.2,1.65,2.10,2.55,2.95]:box('Garage climbing garden cross rail',(1.14,-19,z),(.04,3.6,.035),wood)
lv=[];lf=[];lm=[]
for i in range(240):
y=random.uniform(-20.7,-17.3);z=random.uniform(1.03,2.88);x=1.17+random.uniform(-.05,.20)
if random.random()<.40:z=random.uniform(.96,1.32);x=random.uniform(1.24,2.0)
a=random.uniform(0,2*pi);length=random.uniform(.07,.14);c=Vector((x,y,z));long=Vector((.1,cos(a),sin(a)))*length;wide=Vector((.3,-sin(a),cos(a)))*length*.48;j=len(lv)
lv.extend(tuple(p) for p in [c-long,c+wide,c+long,c-wide]);lf.append((j,j+1,j+2,j+3));lm.append(random.randrange(5))
o=mesh('Garage climbing edible greenery',lv,lf)
for m in leaves:o.data.materials.append(m)
for p,i in zip(o.data.polygons,lm):p.material_index=i
# Bake added curve and text geometry so the fast tour includes every feature.
for col in COL.values():
for o in list(col.objects):
if o.type in {'CURVE','FONT'}:
bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o;bpy.ops.object.convert(target='MESH');o.select_set(False)
terrain=bpy.data.objects['Continuous gently north falling hillside']
for v in terrain.data.vertices:
p=terrain.matrix_world@v.co
if -7.2<p.x<1.2 and -22.7<p.y<-15.3 and p.z>GF-.23:
p.z=GF-.23;v.co=terrain.matrix_world.inverted()@p
terrain.data.update()
cam=bpy.data.objects['PLAY TOUR camera']
for frame,pos,target in [(2401,(-6.5,-12.8,3.00),(-3.7,-19.6,1.45)),(2520,(-6.25,-14.45,2.50),(-3.8,-19.9,1.44))]:
s.frame_set(frame);cam.location=pos;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=frame);cam.keyframe_insert('rotation_euler',frame=frame);cam.data.lens=26;cam.data.keyframe_insert('lens',frame=frame)
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT' and '21 /' in o.data.body:o.data.body='21 / TESLA, SOLAR CHARGING & WORKSHOP'
s['Design direction']='Solarpunk homestead: natural timber and clay, food growing, solar-powered electric mobility, rainwater capture, repair workshop and adaptable modular rooms.'
s['Tesla reference']='Simplified 2020-2024 Model Y inspired concept model, approximately 4.75 m long. https://www.tesla.com/ownersmanual/2020_2024_modely/en_cn/Owners_Manual.pdf'
s['Energy concept']='Eight extra garage solar modules and conceptual EV wall connector. No electrical capacity or charging performance is simulated.'
s.frame_set(1);s.camera=cam
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-detailed.blend')
print('SOLARPUNK_TESLA_BUILT',flush=True)

View File

@@ -0,0 +1,182 @@
[
{
"title": "01 / THE SPACIOUS HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / FUTURE SPACES - BUILD LATER",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / A HOME THAT CAN CHANGE",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
},
{
"title": "25 / TRAINING & STRENGTH",
"start": 2881,
"end": 3000,
"seconds": 5
},
{
"title": "26 / OUTDOOR TABLE & MATCHA",
"start": 3001,
"end": 3120,
"seconds": 5
},
{
"title": "27 / PLAY & FUTURE BASKETBALL",
"start": 3121,
"end": 3240,
"seconds": 5
},
{
"title": "28 / WALKING & FOOD GARDEN",
"start": 3241,
"end": 3360,
"seconds": 5
},
{
"title": "29 / EASY EVERYDAY FOOD",
"start": 3361,
"end": 3480,
"seconds": 5
},
{
"title": "30 / COPPER & MARBLE KITCHEN",
"start": 3481,
"end": 3600,
"seconds": 5
}
]

View File

@@ -0,0 +1,182 @@
[
{
"title": "01 / ONE CONNECTED GARDEN HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / BALANCED FUTURE PAVILIONS",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / RECTANGLES AND GARDEN CURVES",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
},
{
"title": "25 / TRAINING & STRENGTH",
"start": 2881,
"end": 3000,
"seconds": 5
},
{
"title": "26 / OUTDOOR TABLE & MATCHA",
"start": 3001,
"end": 3120,
"seconds": 5
},
{
"title": "27 / FLOWERING GARDEN - ROOM TO BREATHE",
"start": 3121,
"end": 3240,
"seconds": 5
},
{
"title": "28 / FLOWING WALKS AND GARDEN COURT",
"start": 3241,
"end": 3360,
"seconds": 5
},
{
"title": "29 / EASY EVERYDAY FOOD",
"start": 3361,
"end": 3480,
"seconds": 5
},
{
"title": "30 / COPPER & MARBLE KITCHEN",
"start": 3481,
"end": 3600,
"seconds": 5
}
]

View File

@@ -0,0 +1,146 @@
[
{
"title": "01 / THE SPACIOUS HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / FUTURE SPACES - BUILD LATER",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / A HOME THAT CAN CHANGE",
"start": 2641,
"end": 2760,
"seconds": 5
},
{
"title": "24 / BOOKS & A QUIET READING CORNER",
"start": 2761,
"end": 2880,
"seconds": 5
}
]

View File

@@ -0,0 +1,140 @@
[
{
"title": "01 / THE SPACIOUS HOMESTEAD",
"start": 1,
"end": 120,
"seconds": 5
},
{
"title": "02 / LIVING PAVILION & VERANDA",
"start": 121,
"end": 240,
"seconds": 5
},
{
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
"start": 241,
"end": 360,
"seconds": 5
},
{
"title": "04 / DINING & OPEN LIVING",
"start": 361,
"end": 480,
"seconds": 5
},
{
"title": "05 / ONE SERVER - LOCAL AI",
"start": 481,
"end": 600,
"seconds": 5
},
{
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
"start": 601,
"end": 720,
"seconds": 5
},
{
"title": "07 / ADJACENT BEDROOM WING",
"start": 721,
"end": 840,
"seconds": 5
},
{
"title": "08 / ENCLOSED BEDROOM",
"start": 841,
"end": 960,
"seconds": 5
},
{
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
"start": 961,
"end": 1080,
"seconds": 5
},
{
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
"start": 1081,
"end": 1200,
"seconds": 5
},
{
"title": "11 / SAUNA BENCHES & STONE HEATER",
"start": 1201,
"end": 1320,
"seconds": 5
},
{
"title": "12 / OUTDOOR SHOWER",
"start": 1321,
"end": 1440,
"seconds": 5
},
{
"title": "13 / TWO RAINWATER TANKS",
"start": 1441,
"end": 1560,
"seconds": 5
},
{
"title": "14 / ROOFTOP SOLAR & STARLINK",
"start": 1561,
"end": 1680,
"seconds": 5
},
{
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
"start": 1681,
"end": 1800,
"seconds": 5
},
{
"title": "16 / FOOD GARDEN & GREENHOUSE",
"start": 1801,
"end": 1920,
"seconds": 5
},
{
"title": "17 / INSIDE THE GREENHOUSE",
"start": 1921,
"end": 2040,
"seconds": 5
},
{
"title": "18 / QUIET MEDITATION ROOM",
"start": 2041,
"end": 2160,
"seconds": 5
},
{
"title": "19 / CUSHION, CANDLES & STILLNESS",
"start": 2161,
"end": 2280,
"seconds": 5
},
{
"title": "20 / GRAVEL ROAD & GARAGE",
"start": 2281,
"end": 2400,
"seconds": 5
},
{
"title": "21 / CAR, WORKBENCH & BIKE RACKS",
"start": 2401,
"end": 2520,
"seconds": 5
},
{
"title": "22 / FUTURE SPACES - BUILD LATER",
"start": 2521,
"end": 2640,
"seconds": 5
},
{
"title": "23 / A HOME THAT CAN CHANGE",
"start": 2641,
"end": 2760,
"seconds": 5
}
]

View File

@@ -0,0 +1,19 @@
import bpy,json,os
s=bpy.context.scene;out=os.path.dirname(__file__)
chapters=json.load(open(out+'/tour-chapters-daily-life.json'))
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
issues=[];captions=[]
for c in chapters:
frame=c.get('start',c.get('start_frame',1))+30;s.frame_set(frame);bpy.context.view_layer.update()
shown=[o.data.body for o in fonts if o.scale.length>.5]
captions.append({'frame':frame,'titles':shown})
if len(shown)!=1:issues.append({'frame':frame,'titles':shown})
required=['OVEN - insulated glass door','INDUCTION - four zone glass cooktop','ISLAND - honed marble countertop','COPPER STOCKPOT','COPPER SAUCEPAN','COPPER FRYING PAN','PULL UP BAR - dedicated steel frame','OUTDOOR TABLE - ten place timber top','SHADED WALKING LOOP - garden and quiet room','HARVEST BENCH - garden sorting surface','PANTRY - tall timber cabinet','FREEZER - compact upright']
missing=[n for n in required if n not in bpy.data.objects]
report={'chapters':len(chapters),'end_frame':s.frame_end,'fps':s.render.fps,'missing':missing,'caption_issues':issues,'caption_samples':captions,'render_engine':s.render.engine,'render_size':[s.render.resolution_x,s.render.resolution_y],'detail_render_enabled':not bpy.data.collections['DETAIL - full geometry for renders'].hide_render,'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render,'training_steps':sum(o.name.startswith('Training access step') for o in s.objects),'play_steps':sum(o.name.startswith('Play deck access step') for o in s.objects)}
json.dump(report,open(out+'/daily-life-verification.json','w'),indent=2)
assert not missing and not issues,report
assert report['chapters']==30 and s.frame_end==3600 and s.render.fps==24
assert report['detail_render_enabled'] and report['fast_excluded_from_render']
assert report['training_steps']==3 and report['play_steps']==3
print('VERIFIED '+json.dumps({k:v for k,v in report.items() if k!='caption_samples'}),flush=True)

View File

@@ -0,0 +1,30 @@
import bpy,json,os
from mathutils import Vector,Matrix
O=os.path.dirname(__file__);s=bpy.context.scene
chapters=json.load(open(O+'/tour-chapters-garden-flow.json'));fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT'];issues=[]
for c in chapters:
s.frame_set(c['start']+30);shown=[o.data.body for o in fonts if o.scale.length>.5]
if shown!=[c['title']]:issues.append([c['title'],shown])
def ctr(n):
o=bpy.data.objects[n];return Matrix.LocRotScale(o.location,o.rotation_euler.to_quaternion(),o.scale)@(sum((Vector(v) for v in o.bound_box),Vector())/8)
names=[o.name for o in s.objects];garage=ctr('GARAGE - 8m x 7m concrete floor');med=ctr('Meditation | timber insulated floor')
report={'no_table_tennis':not any('table tennis' in n.lower() or n.startswith('PLAY DECK') for n in names),'garage_center':[round(garage.x,2),round(garage.y,2)],'meditation_center':[round(med.x,2),round(med.y,2)],'caption_issues':issues,'tour_chapters':len(chapters),'rainwater_tanks':sum(n.startswith('2000 mm') for n in names),'kitchen_preserved':all(n in names for n in ['ISLAND - honed marble countertop','OVEN - insulated glass door','INDUCTION - four zone glass cooktop']),'flower_beds':sum(n.endswith(' mulched soil') for n in names),'render_size':[s.render.resolution_x,s.render.resolution_y,s.render.resolution_percentage],'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render}
assert report['no_table_tennis'] and not issues and report['rainwater_tanks']==2 and report['kitchen_preserved'] and report['flower_beds']==13
assert report['garage_center']==[0,-19] and report['meditation_center']==[-11.8,12]
assert s.frame_end==3600 and report['fast_excluded_from_render']
layout=json.load(open(O+'/garden-flow-layout.json'))
# Sample the shared paths against the occupied meditation and reserved east pavilion.
collisions=[]
for path in layout['paths']:
if path['kind']!='gravel' or 'future threshold' in path['name']:continue
for x,y in path['points']:
for name,cx in [('meditation',-11.8),('future east',11.8)]:
if abs(x-cx)<2.25 and abs(y-12)<2.4:collisions.append([path['name'],name])
assert not collisions,collisions
report['pavilion_path_intersections']=collisions
for name in ['Eucalyptus pale trunk.006','Eucalyptus pale trunk.007']:
p=ctr(name);assert not (5.75<p.x<13.05 and -21.6<p.y<-16.4),name
report['solar_tree_clearance']=True
s['Daily life additions']='Covered exercise area, outdoor table for ten and tea counter, garden court and flowering beds, curved walking routes and quiet bench, breakfast counter, pantry/freezer and harvest bench. Table-tennis deck removed in version 004.'
s.frame_set(1);bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-solarpunk-garden-flow-tour.blend')
json.dump(report,open(O+'/garden-flow-verification.json','w'),indent=2);print('VERIFIED '+json.dumps(report),flush=True)

View File

@@ -0,0 +1,39 @@
# Samford family homestead — press Play
Open **samford-spacious-guided-tour.blend**. It is saved at frame 1 in the animated camera view.
Place the pointer over the 3D view and press **Space**, or click the Timeline’s Play triangle. Space pauses. **Shift + Left Arrow** returns to the beginning. If you leave camera view, use **View → Cameras → Active Camera** before playing.
The tour lasts **110 seconds**, with **22 chapters**. Each chapter uses a short camera move, with deliberate cuts between rooms. It includes the main living pavilion, kitchen, dining, homelab, short corridor, parents’ bedroom, bathroom, sauna exterior and interior, outdoor shower, tanks, Starlink, solar array, garden, greenhouse, each child’s future room, courtyard and overall site. Chapter labels and Timeline markers are included.
## Final family layout
- Kitchen, dining and lounge form the central shared room.
- The parents’ enclosed bedroom is adjacent, reached by a short corridor approximately 3.5m long and 1.2m wide.
- The bathroom has its own door from the shared lobby. Visitors do not pass through the parents’ bedroom.
- Three future private children’s rooms replace the earlier generic office, guest, workshop and dwelling reservations. Each has a 4.5 × 4.8m footprint, its own front door, bed, desk, wardrobe and personal display area. Colour accents are replaceable examples, with no prescribed theme for any child.
- Covered timber walks connect the three rooms around a courtyard to the shared home. They have no separate kitchens or bathrooms.
- The sauna, shower, two tanks, greenhouse and six garden beds remain spread out. The 30 solar panels and one local-AI server are retained.
These future children’s rooms are fully modelled so their interiors can be toured. Collections 23, 24 and 25 contain them individually; collection 26 contains their shared walks. They are future design proposals, not a claim that they are already built or approved. Their roof-junction finishing strips are in collection 20.
## Files and display
- `family-homestead-overview.png`: rendered overview.
- `three-childrens-rooms.png`: courtyard and children’s rooms.
- `child-room-interior.png`: example adaptable interior.
- `family-courtyard-plan.svg`: labelled layout diagram.
- `tour-chapters.json`: chapter names and frame ranges.
- `tour-checks.json`: verified scene counts, timing, moving cameras and clear corridor doorway.
The interactive tour uses solid material colours for responsiveness. Glass appears outlined in that viewport mode; the Cycles renderer retains actual glazing and procedural materials. Dense ground grass is hidden in the viewport only. The original earlier models are preserved in their own output folders.
## Land search guidance
For two adults and three children wanting this layout, vegetables, fruit trees and general outdoor space, **1–2 hectares of usable land is a reasonable starting search range; about 2 hectares (4.94 acres) is my comfortable planning target**. This is a design estimate, not a statutory minimum or a promise of food self-sufficiency. More land may suit grazing animals or substantial privacy buffers, but pasture quality, water, labour and management become decisive.
A relatively level, usable hectare can offer more practical room than a larger constrained property. Check the actual address for slope, easements, access, wastewater capacity and hazards. [YourHome site selection](https://www.yourhome.gov.au/buy-build-renovate/choosing-site) explains the role of site constraints. [City of Moreton Bay flood mapping](https://www.moretonbay.qld.gov.au/Services/Property-Ownership/Flooding/Flood-Mapping) distinguishes current flood information from planning overlay maps. [Queensland’s small-property livestock guide](https://era.daf.qld.gov.au/id/eprint/8160/1/a-guide-to-managing-livestock-on-small-properties.pdf) explains why livestock capacity is site-dependent.
The detached children’s sleeping rooms need project-specific building and planning classification. Do not assume that describing them as bedrooms makes approval automatic, or that a large lot permits multiple independent dwellings. [Council’s secondary-dwelling guidance](https://www.moretonbay.qld.gov.au/Services/Building-Development/Planning-Schemes/MBRC/Info-Sheets/Secondary-Dwelling) illustrates that dwelling type, zoning and siting all matter; it is not a determination of how this particular bedroom-cabin proposal would be classified.
The central shared room, short private circulation and separation of bedrooms from noisy uses draw on [YourHome orientation guidance](https://www.yourhome.gov.au/passive-design/orientation) and [noise-control guidance](https://www.yourhome.gov.au/live-adapt/noise-control). This remains an architectural concept without a surveyed site, detailed structural design, certified access, fire, moisture, water or off-grid energy assessment.

View File

@@ -0,0 +1,212 @@
import bpy, math, os, json, random, bmesh
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
def move(o,delta): o.location+=Vector(delta)
def shift_collection(name,d):
for o in bpy.data.collections[name].objects: move(o,d)
sleep=(-5,-7,0); sauna=(-5,-2,0); tanks=(-5,-4,0); garden=(6,1,0)
shift_collection('02 Sleeping pavilion 8 x 5 m',sleep)
shift_collection('16 Oval Finnish sauna',sauna)
shift_collection('17 Outdoor shower and stone terrace',sauna)
shift_collection('06 Kitchen garden',garden)
for o in bpy.data.collections['05 Water and services'].objects:
p=center(o); n=o.name
if n.startswith(('2000 mm','Poly tank','Tank service lid','Accessible tank outlet','Gravel tank pad')): move(o,tanks)
elif n.startswith('Exposed rainwater downpipe') and p.x<-10: move(o,sleep)
elif p.y<-8 and ('PV ' in n or 'Photovoltaic' in n or 'Ground array' in n): move(o,(3,-6,0))
for o in bpy.data.collections['18 Starlink and single-server homelab'].objects:
if o.name.startswith('Starlink'): move(o,sleep)
if o.name=='Overview sauna label': o.hide_render=True; o.hide_set(True)
for o in bpy.data.collections['19 Clay finishes and hemp wall infill'].objects:
if o.name.startswith('Sleeping'): move(o,sleep)
for o in list(bpy.data.collections['12 Natural materials and reversible connections'].objects):
p=center(o)
if o.name.startswith('Sleeping west expansion'): move(o,sleep)
elif o.name.startswith('Stone facing bed joint'):
if p.x<-12 and p.y<-5: move(o,sleep)
elif p.x<-6: bpy.data.objects.remove(o,do_unlink=True)
# Guest and office are optional attached rooms; short connecting frames will be rebuilt.
for o in list(bpy.data.collections['13 NEXT - optional attached room modules'].objects):
if o.name.startswith('02A'): move(o,(5,0,0))
elif o.name.startswith('02B'): move(o,(-7,-7,0))
else: bpy.data.objects.remove(o,do_unlink=True)
for o in list(bpy.data.collections['14 LATER - independent dwelling and workshop'].objects):
if o.name.startswith('03A'): move(o,(0,9,0))
elif o.name.startswith('03B'): move(o,(0,-8,0))
else: bpy.data.objects.remove(o,do_unlink=True)
for o in bpy.data.collections['15 Expansion labels - toggle to hide'].objects: o.hide_set(True); o.hide_render=True
# Move the tree whose former location is inside the newly separated guest reservation.
tree_delta=(0,0,0)
for o in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
if 'trunk' in o.name:
p=center(o); tree_delta=(-7,0,0) if -35<p.x<-33 and -17<p.y<-13 else (0,0,0)
if 'Fruit tree' in o.name: tree_delta=garden
if tree_delta!=(0,0,0): move(o,tree_delta)
# Remove old hard-edged routes and rebuild a connected, terrain-following gravel network.
for o in list(bpy.data.collections['07 Terrain and paths'].objects):
if 'path' in o.name.lower() or 'gravel' in o.name.lower(): bpy.data.objects.remove(o,do_unlink=True)
for o in list(bpy.data.collections['17 Outdoor shower and stone terrace'].objects):
if o.name.startswith('Loose gravel sauna access'): bpy.data.objects.remove(o,do_unlink=True)
for o in list(bpy.data.collections['03 Covered breezeway'].objects): bpy.data.objects.remove(o,do_unlink=True)
COL={}; active='new'
for key,name in [('new','20 Spacious site connections'),('bath','21 Bathroom fittings'),('tour','22 PLAY - guided camera tour')]:
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
src=open(os.path.join(OUT,'../samford-modular/build_scene.py')).read(); exec(src[src.index('def mesh('):src.index('timber=textured(')])
wood=bpy.data.materials['Unpainted hardwood - long X grain']; woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain']; steel=bpy.data.materials['Reusable dark steel bolts and shoes']; stone=bpy.data.materials['Split sandstone facing - mineral grain']; roof=bpy.data.materials['Pale Colorbond - powder coated steel']; gravel=bpy.data.materials['Loose local gravel - fine stones']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; F=1.12
for cx,cy,w,d in [(-11.1,-1.8,10.2,1.6),(-16.2,-7.45,1.6,11.3)]:
box('Extended covered walkway bearer',(cx,cy,F-.18),(w,d,.18),wood)
for i in range(int(w/.14)):
box('Extended walkway timber deck board',(cx-w/2+.07+i*.14,cy,F-.035),(.13,d,.06),wood,.003)
box('Extended walkway pale roof',(cx,cy,F+2.7),(w+.20,d+.20,.065),roof,.008)
count=max(2,int(max(w,d)/2.4)+1)
for j in range(count):
a=j/(count-1)
for side in [-1,1]:
x=cx-w/2+.12+a*(w-.24) if w>d else cx+side*(w/2-.12)
y=cy+side*(d/2-.12) if w>d else cy-d/2+.12+a*(d-.24)
g=ground(x,y); box('Walkway stone pier',(x,y,(g+F-.25)/2),(.27,.27,F-.25-g),stone,.01)
box('Walkway exposed timber post',(x,y,F+1.32),(.10,.10,2.64),woodz,.005)
if w>d:
for j in range(int(w/.6)+1): box('Walkway exposed rafter',(cx-w/2+j*.6,cy,F+2.64),(.055,d,.10),wood)
else:
for j in range(int(d/.6)+1): box('Walkway exposed rafter',(cx,cy-d/2+j*.6,F+2.64),(w,.055,.10),wood)
nextmat=bpy.data.materials['NEXT - warm timber ghost frame']
for x1,x2,y in [(6.25,12.8,.8),(-27,-23.25,-15.2)]:
for yy in [y-.8,y+.8]:
beam('Optional extension link floor',(x1,yy,F),(x2,yy,F),.07,.10,nextmat)
beam('Optional extension link roof',(x1,yy,F+2.75),(x2,yy,F+2.75),.07,.10,nextmat)
def path(name,points,width):
vs=[]; fs=[]
for a,b in zip(points,points[1:]):
av,bv=Vector(a),Vector(b); dv=bv-av; side=Vector((-dv.y,dv.x)).normalized()*width/2
n=max(2,int(dv.length/.4))
for i in range(n):
p=av+dv*i/(n-1)
for sidep in [p-side,p+side]: vs.append((sidep.x,sidep.y,ground(sidep.x,sidep.y)+.045))
k=len(vs)-n*2
fs.extend([(k+i*2,k+i*2+1,k+i*2+3,k+i*2+2) for i in range(n-1)])
mesh(name,vs,fs,gravel)
path('Main meandering gravel arrival',[(1,17),(0,10),(0,7)],1.6)
path('Sauna and tanks gravel walk',[(0,7),(-8,6),(-15,6),(-22,6.6),(-25,5.9),(-27,2),(-27,-5),(-24,-8)],1.2)
path('Garden and greenhouse gravel walk',[(0,7),(9,9),(19,9),(26,6),(26,-4),(24,-5)],1.25)
path('Sleeping pavilion garden walk',[(-8,6),(-12,2),(-12,-11),(-15.5,-12.5)],1.1)
path('Future north dwelling reserved access',[(-8,6),(-7,15),(-9,21)],.8)
path('Future workshop reserved access',[(0,-3.5),(0,-18),(0,-25)],.8)
# Stretch ground-contact supports to the actual ground after relocation.
for o in s.objects:
if o.type!='MESH': continue
if any(t in o.name.lower() for t in ['pier','foundation support','tank pad']):
p=center(o); zmin=min((o.matrix_world@v.co).z for v in o.data.vertices); g=ground(p.x,p.y)
if g<max((o.matrix_world@v.co).z for v in o.data.vertices)-.05:
inv=o.matrix_world.inverted()
for v in o.data.vertices:
q=o.matrix_world@v.co
if abs(q.z-zmin)<.001: q.z=g; v.co=inv@q
# Complete the bathroom alcove so the interior tour can identify its actual purpose.
active='bath'; ceramic=mat('Bathroom warm white ceramic',(.76,.73,.65),.23); water=mat('Bathroom satin stainless',(.34,.38,.38),.32,.75)
box('Bathroom mineral floor',(-15.94,-15.92,F+.02),(1.50,3.85,.045),stone,.008)
box('Bathroom toilet cistern',(-15.83,-17.74,F+.65),(.44,.19,.65),ceramic,.08)
box('Bathroom toilet pedestal',(-15.83,-17.38,F+.22),(.29,.40,.43),ceramic,.10)
box('Bathroom toilet bowl',(-15.83,-17.29,F+.43),(.43,.59,.15),ceramic,.15)
box('Bathroom toilet seat',(-15.83,-17.25,F+.52),(.40,.55,.055),ceramic,.12)
box('Bathroom floating timber vanity',(-15.50,-15.7,F+.64),(.45,.82,.28),wood,.02)
box('Bathroom basin',(-15.50,-15.7,F+.85),(.42,.65,.11),ceramic,.055)
rod('Bathroom tap',(-15.30,-15.8,F+.9),(-15.30,-15.8,F+1.13),.015,water)
box('Bathroom mirror',(-15.12,-15.7,F+1.51),(.022,.72,.8),water,.025)
box('Bathroom shower tray',(-15.93,-14.0,F+.04),(1.45,1.18,.08),stone,.012)
rod('Bathroom shower riser',(-15.15,-13.97,F+.35),(-15.15,-13.97,F+2.15),.017,water)
rod('Bathroom shower arm',(-15.15,-13.97,F+2.15),(-15.58,-13.97,F+2.15),.017,water)
rod('Bathroom shower head',(-15.58,-13.97,F+2.10),(-15.58,-13.97,F+2.14),.10,water,sides=24)
# Gentle interior fill lights are visible fixtures, rather than relying on outdoor sunlight.
for name,p,power in [('Living ceiling light',(-2,-.5,3.7),220),('Sleeping ceiling light',(-19,-15,3.7),130),('Bathroom ceiling light',(-15.9,-15.4,3.55),65)]:
d=bpy.data.lights.new(name,'AREA'); d.energy=power; d.size=2; d.color=(1,.85,.68); ob=bpy.data.objects.new(name,d); COL[active].objects.link(ob); ob.location=p
# One animated camera with explicit shot-boundary cuts is dependable in Timeline playback.
active='tour'; s.timeline_markers.clear()
camdata=bpy.data.cameras.new('PLAY TOUR camera'); cam=bpy.data.objects.new('PLAY TOUR camera',camdata); COL[active].objects.link(cam); camdata.clip_start=.04; camdata.clip_end=450; camdata.lens=34; s.camera=cam
shots=[
('01 / THE SPACIOUS HOMESTEAD',(51,71,48),(45,65,43),(-4,-3,1),(-5,-4,1),44),
('02 / LIVING PAVILION & VERANDA',(12,17,6),(8,12,5),(0,3,2),(0,2.8,2),36),
('03 / KITCHEN & EXPOSED STRUCTURE',(-.2,1.8,2.9),(-1.2,1.25,2.75),(-3,-2.35,2.0),(-3.2,-2.35,2.1),30),
('04 / DINING & OPEN LIVING',(-4.8,1.8,2.9),(-4.1,2,2.7),(.2,.1,1.9),(.9,-.1,2),30),
('05 / ONE SERVER - LOCAL AI',(1,-.85,3.55),(1.25,-1,3.3),(3.9,-2.3,1.95),(4.2,-2.3,1.9),36),
('06 / LONG COVERED WALK',(-7.2,-.8,2.75),(-9.5,-.8,2.75),(-16.2,-2.4,2),(-16.2,-4,2),30),
('07 / BEDROOM + BATHROOM PAVILION',(-8,-5,7),(-10,-7,6),(-19,-15.5,2),(-19,-15.5,2),35),
('08 / THE BEDROOM',(-17.6,-13.8,2.8),(-17.9,-14,2.7),(-20,-15.8,1.7),(-20,-16,1.7),26),
('09 / THE BATHROOM',(-16.25,-14.9,2.95),(-16.15,-15.1,2.9),(-15.7,-17,1.7),(-15.7,-17.1,1.7),22),
('10 / FINNISH SAUNA & OUTDOOR SHOWER',(-14,15,6.5),(-16,12,5.5),(-24,2.7,1.7),(-24,2.6,1.6),37),
('11 / SAUNA BENCHES & STONE HEATER',(-23.0,3.62,2.18),(-23.25,3.62,2.18),(-23,1.2,1.95),(-23.2,1.2,1.95),20),
('12 / OUTDOOR SHOWER',(-24.5,7,2.9),(-25,6.5,2.7),(-27,4.4,1.4),(-27,4.4,1.4),34),
('13 / TWO RAINWATER TANKS',(-18,-1,4.4),(-19,-2.5,4.1),(-25,-8,1),(-25,-8,1),40),
('14 / ROOFTOP SOLAR & STARLINK',(-4,-3,10),(-7,-5,9),(-15.5,-13.1,4.6),(-15.5,-13.1,4.6),36),
('15 / SOLAR ARRAY & ENERGY STORAGE',(22,-7,7),(20,-9,6),(13,-17.5,1.5),(13,-17.5,1.5),36),
('16 / FOOD GARDEN & GREENHOUSE',(34,10,9),(32,7,7),(25,-3,1),(25,-4,1),34),
('17 / INSIDE THE GREENHOUSE',(24.2,-5.7,2.6),(24.2,-6.3,2.5),(24.2,-9.0,1.3),(24.2,-9.5,1.3),24),
('18 / ADD ROOMS WHEN NEEDED',(26,12,11),(22,10,10),(15.8,1,2),(15.8,1,2),37),
('19 / FUTURE DWELLING & WORKSHOP',(48,69,51),(44,64,47),(-6,-2,1),(-6,-2,1),40),
('20 / LIVE FIRST. ADD LATER.',(44,64,47),(51,71,51),(-6,-2,1),(-6,-2,1),40),
]
FPS=24; N=120; s.render.fps=FPS; s.frame_start=1; s.frame_end=N*len(shots); s.sync_mode='FRAME_DROP'
caption=mat('Tour captions ivory',(.92,.94,.86),.6); caption.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.92,.94,.86,1); caption.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=1
schedule=[]
for i,(title,a,b,ta,tb,lens) in enumerate(shots):
start=i*N+1; end=(i+1)*N
for frame,pos,target in [(start,a,ta),(end,b,tb)]:
cam.location=pos; cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=frame); cam.keyframe_insert('rotation_euler',frame=frame); camdata.lens=lens; camdata.keyframe_insert('lens',frame=frame)
marker=s.timeline_markers.new(title,frame=start)
cu=bpy.data.curves.new(title,'FONT'); cu.body=title; cu.size=.036; cu.align_x='LEFT'; ob=bpy.data.objects.new(title,cu); COL[active].objects.link(ob); cu.materials.append(caption); ob.parent=cam
half=36/lens; ob.location=(-half*.91,half*(9/16)*.84,-2); ob.show_in_front=True
for f,value in [(0,0),(start-1,0),(start,1),(end,1),(end+1,0)]: ob.scale=(value,)*3; ob.keyframe_insert('scale',frame=f)
schedule.append({'title':title,'start':start,'end':end,'seconds':5})
# Linear moves within a shot; constant title visibility changes.
def curves(action):
if not action: return []
if hasattr(action,'fcurves'): return action.fcurves
result=[]
for layer in action.layers:
for strip in layer.strips:
for slot in action.slots:
bag=strip.channelbag(slot)
if bag: result.extend(bag.fcurves)
return result
for owner in [cam,camdata]:
for fc in curves(owner.animation_data.action):
for k in fc.keyframe_points: k.interpolation='LINEAR'
for o in COL['tour'].objects:
if o.type=='FONT':
for fc in curves(o.animation_data.action):
for k in fc.keyframe_points: k.interpolation='CONSTANT'
# Viewport tour uses material colours for responsive playback. Glazing is outlined so interiors remain visible.
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.display_type='WIRE'
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_set(True)
for screen in bpy.data.screens:
for area in screen.areas:
if area.type=='VIEW_3D':
sp=area.spaces.active; sp.region_3d.view_perspective='CAMERA'; sp.region_3d.view_camera_zoom=0; sp.shading.type='SOLID'; sp.shading.color_type='MATERIAL'; sp.shading.light='STUDIO'; sp.shading.show_shadows=True; sp.shading.show_cavity=True; sp.overlay.show_overlays=False; sp.clip_end=450
if hasattr(sp,'use_local_camera'): sp.use_local_camera=False
for area in screen.areas:
if area.type=='DOPESHEET_EDITOR': area.spaces.active.show_region_ui=False
s.frame_set(1); s.render.engine='CYCLES'; s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True
s['PLAY THE TOUR']='Press Space with the mouse over the 3D viewport, or click Play in Timeline. Shift+Left Arrow returns to the beginning. 100 seconds / 20 scenes at 24 fps. Saved in active camera view.'
s['Spacious layout']='Sleeping pavilion moved 5m west and 7m south: 10m north-south clear gap from living footprint. Covered L walkway rebuilt. Sauna moved 5m west/2m south; tanks 5m west/4m south; garden 6m east/1m north. Optional future sites spaced farther apart.'
s['Adjoining pavilion explanation']='The second rectangle is the bedroom and bathroom pavilion, not a standalone toilet. Bathroom fixtures now modelled for the tour.'
s['Viewport performance']='Dense leaf/grass meshes are hidden only in viewport, retained for final renders. Glass uses outline display in solid viewport. Restore via Alt+H if desired.'
txt=bpy.data.texts.new('START HERE - PRESS PLAY'); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Adjoining pavilion explanation']+'\n\n'+s['Viewport performance'])
json.dump(schedule,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
names=[o.name for o in s.objects]
report={'duration_seconds':100,'frames':s.frame_end,'shots':len(shots),'tank_count':sum(n.startswith('2000 mm') for n in names),'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),'garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),'servers':sum(n.startswith('ONE LOCAL AI SERVER') for n in names),'sleeping_center':[-19,-15.5],'north_south_clear_gap_metres':10,'active_camera':cam.name,'bathroom_fitted':True}
assert report['tank_count']==2 and report['solar_panels']==30 and report['garden_beds']==6 and report['servers']==1
json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
# Contact sheets can be made from quick workbench renders without waiting for a full movie.
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.hide_render=True
if o.name=='Subtle atmosphere over hillside': o.hide_render=True
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_render=True
s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.light='STUDIO'; s.display.shading.color_type='MATERIAL'; s.display.shading.show_shadows=True; s.display.shading.show_cavity=True; s.display.shading.background_type='WORLD'; s.world.color=(.19,.23,.22)
for i in range(len(shots)):
s.frame_set(i*N+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
print('TOUR_BUILT_AND_CHAPTERS_RENDERED',flush=True)

View File

@@ -0,0 +1,151 @@
import bpy, os, math, json, bmesh
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
COL={}
for key,name in [('one','23 FUTURE - Child 1 private room'),('two','24 FUTURE - Child 2 private room'),('three','25 FUTURE - Child 3 private room'),('links','26 Children courtyard and covered links')]:
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
active='links'; src=open(os.path.join(OUT,'../samford-modular/build_scene.py')).read(); exec(src[src.index('def mesh('):src.index('timber=textured(')])
wood=bpy.data.materials['Unpainted hardwood - long X grain']; woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain']; steel=bpy.data.materials['Reusable dark steel bolts and shoes']; stone=bpy.data.materials['Split sandstone facing - mineral grain']; roofmat=bpy.data.materials['Pale Colorbond - powder coated steel']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; weather=bpy.data.materials['Naturally silvered hardwood - horizontal X']; bamboo=bpy.data.materials['Bamboo lining - golden fibres']; linen=bpy.data.materials['Undyed flax linen']; F=1.12
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
for name in ['13 NEXT - optional attached room modules','14 LATER - independent dwelling and workshop']:
c=bpy.data.collections.get(name)
if c:
for o in list(c.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.data.collections.remove(c)
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
if o.name.startswith(('Optional extension link','Future north dwelling reserved access','Future workshop reserved access')): bpy.data.objects.remove(o,do_unlink=True)
# Repair the old west wall boolean so the short corridor is physically open to the main room.
wall=bpy.data.objects['Insulated end wall']; bm=bmesh.new(); bm.from_mesh(wall.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(wall.data); bm.free()
cut=box('TEMP correct central-room doorway',(-6,-1.8,F+1.16),(.85,1.13,2.34),None)
mod=wall.modifiers.new('Open doorway into short bedroom corridor','BOOLEAN'); mod.operation='DIFFERENCE'; mod.solver='EXACT'; mod.object=cut; bpy.context.view_layer.objects.active=wall; bpy.ops.object.modifier_apply(modifier=mod.name); bpy.data.objects.remove(cut,do_unlink=True)
accents=[mat('Child 1 example clay accent',(.40,.20,.12),.85),mat('Child 2 example sage accent',(.25,.37,.26),.85),mat('Child 3 example blue accent',(.20,.31,.39),.85)]
cabins=[('one',-11,12),('two',-3,17),('three',7,15)]
for idx,(key,cx,cy) in enumerate(cabins):
active=key; w=4.5; dep=4.8; xs=cx-w/2; xe=cx+w/2; ys=cy-dep/2; yn=cy+dep/2; a=accents[idx]
box('Child %d | timber insulated floor'%(idx+1),(cx,cy,F-.11),(w,dep,.22),wood,.012)
for x in [xs+.20,xe-.20]:
for y in [ys+.20,yn-.20]:
g=ground(x,y); box('Child room stone pier',(x,y,(g+F-.22)/2),(.28,.28,F-.22-g),stone,.01)
box('Child room steel post shoe',(x,y,F-.23),(.18,.18,.09),steel,.005)
# South entry facade has a solid door and a shaded sidelight.
for x,ww in [(cx-1.42,1.66),(cx+1.42,1.66)]: box('Child room south clay wall',(x,ys,F+1.35),(ww,.17,2.7),clay)
box('Child room south door header',(cx,ys,F+2.49),(1.2,.17,.42),clay)
box('Child %d | independent front door'%(idx+1),(cx,ys-.05,F+1.14),(1.04,.05,2.28),wood,.009)
for x in [cx-.56,cx+.56]: box('Child room entry timber jamb',(x,ys-.07,F+1.17),(.07,.12,2.34),woodz,.005)
rod('Child room door handle',(cx+.39,ys-.1,F+1.1),(cx+.25,ys-.1,F+1.1),.015,steel)
# North wall window and east window give two sides for operable ventilation.
for x,ww in [(cx-1.7,1.10),(cx+1.7,1.10)]: box('Child room north clay wall',(x,yn,F+1.5),(ww,.17,3.0),clay)
for z,hh in [(F+.45,.9),(F+2.75,.50)]: box('Child room north window infill',(cx,yn,z),(2.32,.17,hh),clay)
box('Child room north openable glazing',(cx,yn,F+1.7),(2.24,.02,1.55),glass)
for x in [cx-1.16,cx+1.16]: box('Child room north window frame',(x,yn,F+1.7),(.055,.12,1.64),woodz)
for z in [F+.88,F+2.52]: box('Child room north window rail',(cx,yn,z),(2.36,.12,.06),wood)
box('Child room west clay wall',(xs,cy,F+1.43),(.17,dep,2.86),clay)
for y,dd in [(cy-1.68,1.44),(cy+1.68,1.44)]: box('Child room east clay wall',(xe,y,F+1.43),(.17,dd,2.86),clay)
for z,hh in [(F+.5,1),(F+2.59,.54)]: box('Child room east window infill',(xe,cy,z),(.17,1.92,hh),clay)
box('Child room east openable glazing',(xe,cy,F+1.66),(.018,1.90,1.3),glass)
# Exterior timber weatherboards, kept outside the window apertures.
for j in range(18):
z=F+.075+j*.15
for x,ww in [(cx-1.44,1.57),(cx+1.44,1.57)]: box('Child room front weatherboard',(x,ys-.11,z),(ww,.045,.142),weather)
box('Child room west weatherboard',(xs-.11,cy,z),(.045,dep,.142),weather)
for x in [xs,xe]:
for y,h in [(ys,2.75),(yn,3.1)]: box('Child room exposed corner post',(x,y,F+h/2),(.10,.10,h),woodz,.005)
for i in range(8):
x=xs+i*w/7; beam('Child room exposed roof rafter',(x,ys-.3,F+2.73),(x,yn+.3,F+3.13),.055,.15,wood)
for i in range(33):
x=xs+(i+.5)*w/33; beam('Child room bamboo ceiling',(x,ys,F+2.85),(x,yn,F+3.20),w/33-.004,.02,bamboo)
ro=box('Child room simple mono-pitch roof',(cx,cy,F+3.03),(w+.65,dep+.65,.07),roofmat,.008); ro.rotation_euler.x=math.atan(.35/dep)
box('Child room covered entry deck',(cx,ys-.65,F-.035),(2.2,1.3,.07),wood,.009)
canopy=box('Child room entry awning',(cx,ys-.65,F+2.55),(2.4,1.5,.065),roofmat,.01)
# A complete private room with freely replaceable decor.
bx=cx-1.20; by=cy+.25
box('Child %d | single bed base'%(idx+1),(bx,by,F+.21),(1.05,2.10,.36),wood,.02)
box('Child room mattress',(bx,by,F+.45),(1.0,2.02,.19),linen,.06)
box('Child room sample coloured blanket',(bx,by-.30,F+.555),(1.01,1.18,.035),a,.025)
box('Child room pillow',(bx,by+.72,F+.60),(.70,.40,.12),linen,.06)
box('Child room desk',(cx+.90,cy+.82,F+.75),(1.58,.62,.055),wood,.01)
for x in [cx+.22,cx+1.58]: box('Child room desk leg',(x,cy+.82,F+.37),(.05,.51,.74),steel)
box('Child room desk stool',(cx+.9,cy+.12,F+.44),(.40,.40,.055),wood,.01)
for x in [cx+.74,cx+1.06]:
for y in [cy-.04,cy+.28]: box('Child room stool leg',(x,y,F+.21),(.03,.03,.42),wood)
box('Child room wardrobe',(cx+1.60,cy-1.54,F+1.0),(.66,.72,2.0),wood,.012)
box('Child room personal pinboard',(xs+.10,cy-.15,F+1.75),(.035,1.35,.77),a,.01)
for j in range(3): box('Child room replaceable display shelf',(cx+.90,yn-.20,F+1.35+j*.37),(1.6,.28,.025),wood)
cu=bpy.data.curves.new('Child room sign','FONT'); cu.body='CHILD %d / YOUR SPACE'%(idx+1); cu.size=.085; cu.align_x='CENTER'; ob=bpy.data.objects.new(cu.body,cu); COL[active].objects.link(ob); ob.location=(cx,ys-.145,F+2.4); ob.rotation_euler=(pi/2,0,0); cu.materials.append(steel)
ld=bpy.data.lights.new('Child room warm ceiling light','AREA'); ld.energy=120; ld.size=2; ld.color=(1,.86,.7); lo=bpy.data.objects.new(ld.name,ld); COL[active].objects.link(lo); lo.location=(cx,cy,F+2.6)
COL[active]['Purpose']='Future private bedroom for one child, not a separate self-contained dwelling. Shared kitchen/dining and bathroom in the family home. Decor shown is replaceable example only.'
active='links'
for cx,cy,w,dep in [(-3,10,1.2,9.2),(-7,9,8,1.2),(2,9,10,1.2),(7,10.8,1.2,3.6)]:
box('Children shared covered walk deck',(cx,cy,F-.07),(w,dep,.14),wood,.008)
box('Children shared covered walk roof',(cx,cy,F+2.55),(w+.14,dep+.14,.055),roofmat,.008)
n=max(2,int(max(w,dep)/2.7)+1)
for j in range(n):
for side in [-1,1]:
x=cx-w/2+.10+j*(w-.20)/(n-1) if w>dep else cx+side*(w/2-.08)
y=cy+side*(dep/2-.08) if w>dep else cy-dep/2+.10+j*(dep-.20)/(n-1)
g=ground(x,y); box('Children walkway post',(x,y,(g+F+2.52)/2),(.075,.075,F+2.52-g),woodz,.004)
# Courtyard-facing directions keep the children connected to the family common room.
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data
newshots={
17:('18 / CHILD 1 - THEIR OWN ROOM',(-10.15,10.30,2.9),(-10.35,10.5,2.8),(-11.45,12.5,1.8),(-11.5,12.7,1.8),24),
18:('19 / CHILD 2 - THEIR OWN ROOM',(-2.15,15.30,2.9),(-2.35,15.5,2.8),(-3.45,17.5,1.8),(-3.5,17.7,1.8),24),
19:('20 / CHILD 3 - THEIR OWN ROOM',(7.85,13.30,2.9),(7.65,13.5,2.8),(6.55,15.5,1.8),(6.5,15.7,1.8),24),
20:('21 / THREE PRIVATE ROOMS - ONE FAMILY',(18,-1,14),(15,2,12),(-2,13,2),(-2,13,2),37),
21:('22 / LIVE FIRST. GROW TOGETHER.',(44,64,47),(51,71,51),(-4,0,1),(-4,0,1),40),
}
tour=bpy.data.collections['22 PLAY - guided camera tour']; caption=bpy.data.materials['Tour captions ivory']
for o in list(tour.objects):
if o.type=='FONT' and int(o.name[:2])>=18: bpy.data.objects.remove(o,do_unlink=True)
for marker in list(s.timeline_markers):
if marker.frame>=2041: s.timeline_markers.remove(marker)
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))[:17]
for i,(title,a,b,ta,tb,lens) in newshots.items():
start=i*120+1; end=(i+1)*120
for frame,pos,target in [(start,a,ta),(end,b,tb)]:
cam.location=pos; cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=frame); cam.keyframe_insert('rotation_euler',frame=frame); data.lens=lens; data.keyframe_insert('lens',frame=frame)
s.timeline_markers.new(title,frame=start)
cu=bpy.data.curves.new(title,'FONT'); cu.body=title; cu.size=.036; ob=bpy.data.objects.new(title,cu); tour.objects.link(ob); ob.parent=cam; half=36/lens; ob.location=(-half*.91,half*9/16*.84,-2); ob.show_in_front=True; cu.materials.append(caption)
for f,val in [(0,0),(start-1,0),(start,1),(end,1),(end+1,0)]: ob.scale=(val,)*3; ob.keyframe_insert('scale',frame=f)
chapters.append({'title':title,'start':start,'end':end,'seconds':5})
def curves(action):
if hasattr(action,'fcurves'): return action.fcurves
result=[]
for layer in action.layers:
for strip in layer.strips:
for slot in action.slots:
bag=strip.channelbag(slot)
if bag: result.extend(bag.fcurves)
return result
for owner in [cam,data]:
for fc in curves(owner.animation_data.action):
for k in fc.keyframe_points: k.interpolation='LINEAR'
for o in tour.objects:
if o.type=='FONT':
for fc in curves(o.animation_data.action):
for k in fc.keyframe_points: k.interpolation='CONSTANT'
# Correct normals on newly made solids; outline transparent glass in fast viewport mode.
for c in COL.values():
for o in c.objects:
if o.type=='MESH':
bm=bmesh.new(); bm.from_mesh(o.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(o.data); bm.free()
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.display_type='WIRE'
s.frame_end=2640; s.frame_set(1); s.camera=cam; s.sync_mode='FRAME_DROP'
s['PLAY THE TOUR']='Press Space over the 3D viewport or click Timeline Play. Shift+Left Arrow returns to frame 1. 110 seconds, 22 chapters, 24 fps. Saved in camera view.'
s['Future additions']='Exactly three separate children bedroom cabins, one for each child. 4.5m x 4.8m footprints, private doors, bed, desk, wardrobe and personal display area. Covered walks connect them to the shared home. No extra kitchens/bathrooms in the cabins.'
s['Land planning']='Concept can be investigated on roughly 1-2 hectares of usable land; 2 hectares is a comfortable search target, not a site-specific sufficiency or planning guarantee. No cadastral boundaries or survey supplied.'
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Future additions']+'\n\n'+s['Land planning']+'\n\n'+s['Viewport performance'])
json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
report=json.load(open(os.path.join(OUT,'tour-checks.json'))); report.update({'duration_seconds':110,'frames':2640,'shots':22,'future_child_rooms':3,'child_room_footprints':[4.5,4.8],'generic_future_office_guest_workshop_removed':True}); json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
s.render.engine='CYCLES'; s.render.resolution_percentage=100
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
# All final chapter thumbnails, plus motion endpoints for the corridor and children rooms.
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.hide_render=True
if o.name=='Subtle atmosphere over hillside': o.hide_render=True
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_render=True
s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.light='STUDIO'; s.display.shading.color_type='MATERIAL'; s.display.shading.show_shadows=True; s.display.shading.show_cavity=True
for i in range(22):
s.frame_set(i*120+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
print('FAMILY_TOUR_FINAL_REVIEW_READY',flush=True)

View File

@@ -0,0 +1,40 @@
from pathlib import Path
from html import escape
O=Path(__file__).parent
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1080" viewBox="0 0 1400 1080"><rect width="1400" height="1080" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}.small{font-size:13px}.body{font-size:16px}.title{font-size:32px;font-weight:600}.label{font-size:13px;font-weight:600}</style><text x="60" y="60" class="title">One family. Room to become yourself.</text><text x="60" y="94" class="body">SAMFORD VALLEY / Shared kitchen + dining, adjacent parents’ room, three future children’s rooms</text>']
def pt(x,y):return (690+17*x,520-17*y)
def label(x,y,lines,size=13):
X,Y=pt(x,y)
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
def rect(cx,cy,w,d,fill,stroke='#4b6256',dash=''):
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*17}" height="{d*17}" rx="2" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
def line(points,color='#a99372',width=16):
ps=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{ps}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
# Covered walks first, with clear building footprints drawn over them.
line([(-3,5.4),(-3,14.6)],width=20);line([(-11,9), (7,9),(7,12.6)],width=20)
rect(0,0,12,6,'#ddc8a9');label(0,.8,['CENTRAL ROOM','Kitchen · dining · lounge','Local AI desk'],13)
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['NORTH VERANDA'],12)
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.9,1.0,['Short corridor','3.5 × 1.2 m'],12)
rect(-11.8,-4.9,8,5,'#ddc8a9');rect(-12.7,-4.9,6.2,5,'#ddc8a9');label(-12.7,-4.5,['PARENTS’','BEDROOM'],12)
rect(-8.7,-5.63,1.8,3.54,'#b6c4c5');label(-8.7,-5.25,['Bath'],10);label(-8.7,-3.0,['Lobby'],9)
for i,(x,y,color) in enumerate([(-11,12,'#d8b4a0'),(-3,17,'#b9c9b8'),(7,15,'#aec4d0')]):
rect(x,y,4.5,4.8,color,dash='stroke-dasharray="6 3"');label(x,y+.6,[f'CHILD {i+1}','Own room','4.5 × 4.8 m'],11)
label(-1,11.6,['SHARED','COURTYARD'],12)
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="38" ry="32" fill="#c5a378" stroke="#4b6256" stroke-width="2"/>');label(-23,2.1,['SAUNA'],12)
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7,['Outdoor','shower'],12)
for x in [-23.95,-26.25]:
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="17" fill="#6b7f61"/>')
label(-25,-10,['Rainwater tanks'],12)
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7,['GREENHOUSE'],11)
for x in [23.8,27.5]:
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
label(25.5,7,['FOOD GARDEN'],13)
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17,['GROUND SOLAR'],12)
label(0,-7.5,['Roof solar on main pavilion','Starlink on parents’ wing'],12)
line([(0,7),(9,9),(19,9),(26,6)],color='#d2ccb9',width=7)
line([(-3,5.4),(-15,6),(-22,6.6),(-25,5.9)],color='#d2ccb9',width=7)
# Diagram furniture indicates that children personalise the same simple base room.
for x,y in [(-11,12),(-3,17),(7,15)]:
rect(x-1.4,y-.7,.7,1.5,'#f3eadb',stroke='#879387')
p.extend(['<path d="M1260 215V148m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1260" y="135" text-anchor="middle" class="body">N</text>','<line x1="75" y1="890" x2="245" y2="890" stroke="#263a34" stroke-width="4"/><text x="75" y="920" class="small">10 metres</text>','<line x1="60" y1="960" x2="1340" y2="960" stroke="#bdc3b6"/><text x="60" y="992" class="body">Dashed cabins = future rooms for the three children. Shared kitchen, dining and bathroom stay in the family home.</text>','<text x="60" y="1020" class="small">Illustrative layout, not a surveyed block or construction drawing. No property boundary or approval is implied.</text>','<text x="60" y="1044" class="small">Land search guide: 1–2 hectares of usable land; around 2 hectares offers more flexibility. Site conditions decide suitability.</text></svg>'])
(O/'family-courtyard-plan.svg').write_text(''.join(p))

View File

@@ -0,0 +1,70 @@
import bpy, os, math, bmesh, json
from mathutils import Vector
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
c=bpy.data.collections['20 Spacious site connections']
clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; wood=bpy.data.materials['Unpainted hardwood - long X grain']
def box(n,p,dims,mat):
bpy.ops.mesh.primitive_cube_add(size=1,location=p); o=bpy.context.object; o.name=n; o.dimensions=dims; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
for col in list(o.users_collection): col.objects.unlink(o)
c.objects.link(o); o.data.materials.append(mat); return o
F=1.12
# Explicit wall segments preserve a real, unobstructed doorway without boolean ambiguity.
o=bpy.data.objects.get('Insulated end wall')
if o: bpy.data.objects.remove(o,do_unlink=True)
for ya,yb,zlo in [(-3,-2.365,F),(-1.235,3,F),(-2.365,-1.235,F+2.34)]:
def top(y): return F+2.7+(y+3)*.7/6
vs=[(x,y,z) for x in [-6.09,-5.91] for y,z in [(ya,zlo),(yb,zlo),(yb,top(yb)),(ya,top(ya))]]
me=bpy.data.meshes.new('Living west wall with clear doorway'); me.from_pydata(vs,[],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)]); me.materials.append(clay); ob=bpy.data.objects.new('Living west wall | clear corridor opening',me); c.objects.link(ob)
o=bpy.data.objects.get('Short corridor north clay wall')
if o: bpy.data.objects.remove(o,do_unlink=True)
box('Corridor north wall below high window',(-7.75,-1.16,F+.895),(3.5,.12,1.79),clay)
box('Corridor north wall above high window',(-7.75,-1.16,F+2.425),(3.5,.12,.15),clay)
for x in [-9.31,-6.19]: box('Corridor north wall window end',(x,-1.16,F+2.07),(.38,.12,.56),clay)
o=bpy.data.objects['Bedroom privacy partition south']; o.location.y=-5.61; o.dimensions.y=3.58
o=bpy.data.objects['Bedroom privacy partition north return']; o.location.y=-2.57; o.dimensions.y=.34
for x in [-9.2,-8.22]: box('Bathroom solid door jamb',(x,-3.85,F+1.14),(.05,.18,2.28),wood)
# Close the sloping wall/roof junctions on each future child's enclosed room.
for cx,cy in [(-11,12),(-3,17),(7,15)]:
for xx in [cx-2.25,cx+2.25]:
ya,yb=cy-2.4,cy+2.4
vs=[(x,y,z) for x in [xx-.085,xx+.085] for y,z in [(ya,F+2.60),(yb,F+2.60),(yb,F+3.22),(ya,F+2.87)]]
me=bpy.data.meshes.new('Sloping sealed child wall top'); me.from_pydata(vs,[],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)]); me.materials.append(clay); ob=bpy.data.objects.new('Child room sealed sloping wall top',me); c.objects.link(ob)
box('Child room north wall roof junction',(cx,cy+2.4,F+3.10),(4.5,.17,.22),clay)
box('Child room south wall roof junction',(cx,cy-2.4,F+2.78),(4.5,.17,.17),clay)
for ob in s.objects:
if ob.type=='MESH' and len(ob.data.vertices)==8 and len(ob.data.polygons)==6:
bm=bmesh.new(); bm.from_mesh(ob.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(ob.data); bm.free()
if ob.type=='MESH' and ob.name.startswith(('Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): ob.hide_set(False)
cam=s.camera; data=cam.data
for f,pos,target in [(601,(-6.6,-1.8,2.70),(-8.9,-2.05,2.13)),(720,(-7.7,-1.8,2.65),(-8.9,-2.7,2.10))]:
cam.location=pos; cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=f); cam.keyframe_insert('rotation_euler',frame=f)
# Update saved viewport state for Play and retain the leafy canopy in the interactive scene.
for screen in bpy.data.screens:
for area in screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.region_3d.view_perspective='CAMERA'; area.spaces.active.shading.type='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.overlay.show_overlays=False
s['Viewport performance']='Material-colour camera tour. Dense grass is hidden only in the viewport; canopy remains visible. Glazing uses outlines in solid viewport and renders as glass in Cycles.'
# Check the doorway's centre ray reaches the corridor interior, not a wall at its threshold.
dg=bpy.context.evaluated_depsgraph_get(); start=Vector((-5.5,-1.8,2.3)); hit=s.ray_cast(dg,start,Vector((-1,0,0)),distance=4.2)
assert not hit[0] or hit[1].x<-7.9, (hit[4].name,hit[1][:])
report=json.load(open(os.path.join(OUT,'tour-checks.json'))); report['doorway_clear']=True
positions=[]
for ch in json.load(open(os.path.join(OUT,'tour-chapters.json'))):
s.frame_set(ch['start']); a=list(cam.location); s.frame_set(ch['end']); b=list(cam.location); assert (Vector(a)-Vector(b)).length>.01; positions.append({'chapter':ch['title'],'start_camera':a,'end_camera':b})
report['all_chapters_have_camera_motion']=True; report['render_resolution']=[2560,1440]; json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
s.frame_set(1); s.render.engine='CYCLES'; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True; s.cycles.adaptive_threshold=.045
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
for dev in prefs.devices: dev.use=dev.type=='METAL'
s.cycles.device='GPU'; s.render.filepath=os.path.join(OUT,'family-homestead-overview.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
bpy.ops.render.render(write_still=True)
print('FAMILY_OVERVIEW_RENDERED',flush=True)
s.frame_set(2460); s.render.filepath=os.path.join(OUT,'three-childrens-rooms.png'); bpy.ops.render.render(write_still=True)
print('CHILDREN_COURTYARD_RENDERED',flush=True)
s.frame_set(2160); s.render.filepath=os.path.join(OUT,'child-room-interior.png'); bpy.ops.render.render(write_still=True)
print('CHILD_ROOM_INTERIOR_RENDERED',flush=True)
# One last fast screenshot verifies the revised short-corridor chapter.
for ob in s.objects:
if ob.name=='Subtle atmosphere over hillside': ob.hide_render=True
s.frame_set(660); s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.color_type='MATERIAL'; s.render.filepath=os.path.join(OUT,'corridor-check.png'); bpy.ops.render.render(write_still=True)
print('FINAL_TOUR_VERIFIED',flush=True)

View File

@@ -0,0 +1,6 @@
import bpy,json
from mathutils import Vector
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
report={c.name:[{'name':o.name,'center':[round(v,2) for v in center(o)],'location':[round(v,2) for v in o.location],'dimensions':[round(v,2) for v in o.dimensions]} for o in c.objects if o.type in ['MESH','CAMERA','LIGHT','FONT']] for c in bpy.data.collections}
json.dump(report,open('/tmp/samford-scene-inventory.json','w'),indent=2)
print({c.name:len(c.objects) for c in bpy.data.collections})

View File

@@ -0,0 +1,114 @@
import bpy, math, os, json
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
d=Vector((7.2,10.6,0))
for name in ['02 Sleeping pavilion 8 x 5 m','21 Bathroom fittings']:
for o in bpy.data.collections[name].objects:
if name.startswith('21') and o.type=='LIGHT' and o.name.startswith('Living'): continue
o.location+=d
for o in bpy.data.collections['18 Starlink and single-server homelab'].objects:
if o.name.startswith('Starlink'): o.location+=d
for o in bpy.data.collections['19 Clay finishes and hemp wall infill'].objects:
if o.name.startswith('Sleeping'): o.location+=d
for o in bpy.data.collections['12 Natural materials and reversible connections'].objects:
p=center(o)
if o.name.startswith('Sleeping west expansion') or (o.name.startswith('Stone facing bed joint') and p.x<-16 and p.y<-11): o.location+=d
for o in bpy.data.collections['05 Water and services'].objects:
if o.name.startswith('Exposed rainwater downpipe') and center(o).x<-15: o.location+=d
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
if any(o.name.startswith(n) for n in ['Extended','Walkway','Sleeping pavilion garden walk']): bpy.data.objects.remove(o,do_unlink=True)
elif o.name.startswith('Optional extension link') and center(o).x<0: bpy.data.objects.remove(o,do_unlink=True)
# Main common room remains undivided; the private bedroom becomes fully enclosed.
for o in list(bpy.data.collections['02 Sleeping pavilion 8 x 5 m'].objects):
if o.name=='Bedroom bathroom partition': bpy.data.objects.remove(o,do_unlink=True)
COL={'room':bpy.data.collections['21 Bathroom fittings'],'link':bpy.data.collections['20 Spacious site connections']}; active='link'
src=open(os.path.join(OUT,'../samford-modular/build_scene.py')).read(); exec(src[src.index('def mesh('):src.index('timber=textured(')])
wood=bpy.data.materials['Unpainted hardwood - long X grain']; woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain']; steel=bpy.data.materials['Reusable dark steel bolts and shoes']; stone=bpy.data.materials['Split sandstone facing - mineral grain']; roof=bpy.data.materials['Pale Colorbond - powder coated steel']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; F=1.12
# Short enclosed link: west door of central room to north entrance of bedroom wing.
box('SHORT CORRIDOR - 3.5m long x 1.2m wide',(-7.75,-1.8,F-.08),(3.5,1.2,.16),wood,.012)
box('Short corridor insulated roof',(-7.75,-1.8,F+2.52),(3.65,1.37,.10),roof,.008)
box('Short corridor north clay wall',(-7.75,-1.16,F+1.25),(3.5,.12,2.50),clay,.005)
box('Short corridor south clay wall',(-6.95,-2.44,F+1.25),(1.60,.12,2.50),clay,.005)
box('Short corridor west return',(-9.56,-1.8,F+1.25),(.12,1.4,2.50),clay,.005)
for x in [-9.4,-6.2]:
for y in [-2.31,-1.29]:
g=ground(x,y); box('Short corridor stone pier',(x,y,(g+F-.16)/2),(.25,.25,F-.16-g),stone,.01)
box('Short corridor exposed post',(x,y,F+1.25),(.085,.085,2.50),woodz,.005)
for x in [-9.2,-8.4,-7.6,-6.8]: box('Short corridor exposed ceiling rafter',(x,-1.8,F+2.46),(.06,1.2,.12),wood)
# High glazing in north wall admits daylight while preserving privacy.
cut=box('TEMP corridor high light cutter',(-7.75,-1.16,F+2.07),(2.65,.5,.55),None)
wall=bpy.data.objects['Short corridor north clay wall']; mod=wall.modifiers.new('Clerestory opening','BOOLEAN'); mod.object=cut; mod.operation='DIFFERENCE'; bpy.context.view_layer.objects.active=wall; bpy.ops.object.modifier_apply(modifier=mod.name); bpy.data.objects.remove(cut,do_unlink=True)
win=box('Short corridor high daylight glazing',(-7.75,-1.16,F+2.07),(2.61,.018,.51),glass); win.display_type='WIRE'
for z in [F+1.80,F+2.34]: box('Corridor clerestory timber rail',(-7.75,-1.16,z),(2.73,.08,.055),wood)
# Privacy doors to two separate rooms, both reached from a shared entrance lobby.
active='room'
box('Bedroom privacy partition south',(-9.6,-5.79,F+1.34),(.15,3.22,2.68),clay,.006)
box('Bedroom privacy partition north return',(-9.6,-2.51,F+1.34),(.15,.22,2.68),clay,.006)
box('Bedroom privacy door lintel',(-9.6,-3.29,F+2.50),(.15,1.35,.32),clay)
door=box('BEDROOM - independent solid timber door',(-9.6,-3.28,F+1.13),(.05,1.02,2.26),wood,.008)
for y in [-3.82,-2.74]: box('Bedroom privacy door jamb',(-9.6,y,F+1.16),(.19,.06,2.32),woodz,.006)
rod('Bedroom door lever',(-9.66,-2.92,F+1.06),(-9.78,-2.92,F+1.06),.015,steel)
box('Bathroom lobby partition east',(-7.99,-3.85,F+1.34),(.37,.14,2.68),clay)
box('Bathroom lobby partition west',(-9.40,-3.85,F+1.34),(.40,.14,2.68),clay)
box('Bathroom lobby partition header',(-8.72,-3.85,F+2.49),(1.02,.14,.34),clay)
box('BATHROOM - separate door from lobby',(-8.71,-3.85,F+1.12),(.95,.05,2.24),wood,.006)
rod('Bathroom door lever',(-8.38,-3.81,F+1.05),(-8.20,-3.81,F+1.05),.015,steel)
# Relocate wet fixtures deeper than the shared lobby, keeping the toilet out of the common-room sightline.
for o in COL['room'].objects:
if o.name.startswith('Bathroom shower'):
o.location.y-=1.15
if o.name=='Bathroom mineral floor': o.location.y=-5.70; o.dimensions.y=3.18
# A wardrobe buffers the bed from the bathroom plumbing wall.
for y in [-6.55,-5.70,-4.85]:
box('Bedroom wardrobe cabinet',(-10.06,y,F+1.12),(.52,.81,2.24),wood,.009)
box('Bedroom wardrobe discreet handle',(-10.34,y+.27,F+1.15),(.028,.025,.20),steel,.005)
# Move dining slightly into the visual centre without changing generous kitchen circulation.
for o in bpy.data.collections['10 Furniture'].objects:
if o.name.startswith(('Dining table','Timber dining chair','Chair leg','Chair back')): o.location.x+=.30
# Replace long-route chapter with short lobby views and reposition bedroom/bathroom/Starlink shots.
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data
changes={
5:('06 / SHORT CORRIDOR TO PRIVATE ROOMS',(-5.6,-1.8,2.78),(-6.05,-1.8,2.72),(-8.7,-1.8,2.1),(-8.9,-2.3,2.1),24),
6:('07 / ADJACENT BEDROOM WING',(-1,9,8),(-3,7,7),(-10,-3.5,2),(-11,-4,2),34),
7:('08 / ENCLOSED BEDROOM',(-10.9,-3.3,2.9),(-11.2,-3.5,2.75),(-13.9,-5.2,1.8),(-13.9,-5.4,1.8),27),
8:('09 / BATHROOM - SEPARATE LOBBY DOOR',(-9.0,-4.15,2.85),(-8.98,-4.35,2.75),(-8.52,-6.4,1.65),(-8.52,-6.55,1.65),22),
13:('14 / ROOFTOP SOLAR & STARLINK',(3,7,10),(0,6,9),(-4,-1,4.3),(-6,-1.5,4.3),35),
}
for i,(title,a,b,ta,tb,lens) in changes.items():
start=i*120+1; end=(i+1)*120
for f,p,t in [(start,a,ta),(end,b,tb)]:
cam.location=p; cam.rotation_euler=(Vector(t)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=f); cam.keyframe_insert('rotation_euler',frame=f); data.lens=lens; data.keyframe_insert('lens',frame=f)
for marker in s.timeline_markers:
if marker.frame==start: marker.name=title
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if o.type=='FONT' and o.name.startswith('%02d /'%(i+1)):
o.data.body=title; o.name=title; half=36/lens; o.location=(-half*.91,half*9/16*.84,-2)
# Keep caption viewport state; disable the misleading legacy long-gap description.
s['Spacious layout']='Final revision: central kitchen/dining/living pavilion; enclosed adjacent bedroom wing via a short 3.5m x 1.2m corridor and shared lobby. Bathroom has its own door from lobby. Sauna, tanks, garden and future dwellings remain spread out.'
s['Adjoining pavilion explanation']='Adjacent bedroom wing contains a private bedroom, a shared entrance lobby and separately accessed bathroom. It is not a detached toilet block.'
s['Architecture rationale']='North-facing common room and veranda retained; short circulation; solid privacy doors; wardrobe between bed and wet-service wall; existing openable facade retained. Site-specific structural, moisture, fire and access design still required.'
for name in ['Sleeping ceiling light','Bathroom ceiling light']:
o=bpy.data.objects.get(name)
if o: o.location.y-=.6
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.display_type='WIRE'
s.frame_set(1); s.camera=cam; s.render.engine='CYCLES'; s.render.resolution_percentage=100
checks=json.load(open(os.path.join(OUT,'tour-checks.json'))); checks.pop('north_south_clear_gap_metres',None); checks.update({'sleeping_center':[-11.8,-4.9],'short_corridor_metres':[3.5,1.2],'bedroom_separate_room':True,'bathroom_access':'Own door from shared lobby; no passage through bedroom','layout_revision':'Adjacent private bedroom and central kitchen/dining; dispersed outbuildings'})
json.dump(checks,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))
for i,v in changes.items(): chapters[i]['title']=v[0]
json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Adjoining pavilion explanation']+'\n\n'+s['Viewport performance'])
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
# Review every chapter with the final layout.
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.hide_render=True
if o.name=='Subtle atmosphere over hillside': o.hide_render=True
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_render=True
s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.light='STUDIO'; s.display.shading.color_type='MATERIAL'; s.display.shading.show_shadows=True; s.display.shading.show_cavity=True
for i in range(20):
s.frame_set(i*120+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
print('ADJACENT_BEDROOM_TOUR_REVIEW_READY',flush=True)

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