Preserve homestead context and garden-centred Blender design
This commit is contained in:
36
source/samford-retreat-garage/README.md
Normal file
36
source/samford-retreat-garage/README.md
Normal 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.
|
||||
117
source/samford-retreat-garage/UNREAL-PROJECT-SCOPE.md
Normal file
117
source/samford-retreat-garage/UNREAL-PROJECT-SCOPE.md
Normal 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.
|
||||
218
source/samford-retreat-garage/add_daily_life.py
Normal file
218
source/samford-retreat-garage/add_daily_life.py
Normal 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)
|
||||
102
source/samford-retreat-garage/add_reading_corner.py
Normal file
102
source/samford-retreat-garage/add_reading_corner.py
Normal 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)
|
||||
11
source/samford-retreat-garage/bedroom-sleep-checks.json
Normal file
11
source/samford-retreat-garage/bedroom-sleep-checks.json
Normal 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
|
||||
}
|
||||
105
source/samford-retreat-garage/bedroom_sleep_setup.py
Normal file
105
source/samford-retreat-garage/bedroom_sleep_setup.py
Normal 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)
|
||||
54
source/samford-retreat-garage/create_garden_plan.py
Normal file
54
source/samford-retreat-garage/create_garden_plan.py
Normal 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))
|
||||
64
source/samford-retreat-garage/create_site_plan.py
Normal file
64
source/samford-retreat-garage/create_site_plan.py
Normal 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))
|
||||
34
source/samford-retreat-garage/daily-life-checks.json
Normal file
34
source/samford-retreat-garage/daily-life-checks.json
Normal 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
|
||||
}
|
||||
}
|
||||
97
source/samford-retreat-garage/daily-life-layout.json
Normal file
97
source/samford-retreat-garage/daily-life-layout.json
Normal 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
|
||||
}
|
||||
198
source/samford-retreat-garage/daily-life-verification.json
Normal file
198
source/samford-retreat-garage/daily-life-verification.json
Normal 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
|
||||
}
|
||||
30
source/samford-retreat-garage/design-checks.json
Normal file
30
source/samford-retreat-garage/design-checks.json
Normal 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
|
||||
}
|
||||
127
source/samford-retreat-garage/enhance_kitchen.py
Normal file
127
source/samford-retreat-garage/enhance_kitchen.py
Normal 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.'
|
||||
71
source/samford-retreat-garage/export_meditation_unreal.py
Normal file
71
source/samford-retreat-garage/export_meditation_unreal.py
Normal 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)
|
||||
60
source/samford-retreat-garage/finish_and_render.py
Normal file
60
source/samford-retreat-garage/finish_and_render.py
Normal 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)
|
||||
11
source/samford-retreat-garage/garden-flow-checks.json
Normal file
11
source/samford-retreat-garage/garden-flow-checks.json
Normal 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
|
||||
}
|
||||
5118
source/samford-retreat-garage/garden-flow-layout.json
Normal file
5118
source/samford-retreat-garage/garden-flow-layout.json
Normal file
File diff suppressed because it is too large
Load Diff
24
source/samford-retreat-garage/garden-flow-verification.json
Normal file
24
source/samford-retreat-garage/garden-flow-verification.json
Normal 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
|
||||
}
|
||||
25
source/samford-retreat-garage/meditation_finish.py
Normal file
25
source/samford-retreat-garage/meditation_finish.py
Normal 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)
|
||||
100
source/samford-retreat-garage/optimize_playback.py
Normal file
100
source/samford-retreat-garage/optimize_playback.py
Normal 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)
|
||||
26
source/samford-retreat-garage/performance-checks.json
Normal file
26
source/samford-retreat-garage/performance-checks.json
Normal 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."
|
||||
}
|
||||
8
source/samford-retreat-garage/reading-corner-checks.json
Normal file
8
source/samford-retreat-garage/reading-corner-checks.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"books": 163,
|
||||
"bookshelf_bays": 2,
|
||||
"reading_chairs": 1,
|
||||
"tour_chapters": 24,
|
||||
"tour_seconds": 120,
|
||||
"fast_proxy_objects_added": 1
|
||||
}
|
||||
234
source/samford-retreat-garage/refine_garden_flow.py
Normal file
234
source/samford-retreat-garage/refine_garden_flow.py
Normal 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)
|
||||
269
source/samford-retreat-garage/revise_scene.py
Normal file
269
source/samford-retreat-garage/revise_scene.py
Normal 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)
|
||||
139
source/samford-retreat-garage/solarpunk_tesla.py
Normal file
139
source/samford-retreat-garage/solarpunk_tesla.py
Normal 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)
|
||||
182
source/samford-retreat-garage/tour-chapters-daily-life.json
Normal file
182
source/samford-retreat-garage/tour-chapters-daily-life.json
Normal 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
|
||||
}
|
||||
]
|
||||
182
source/samford-retreat-garage/tour-chapters-garden-flow.json
Normal file
182
source/samford-retreat-garage/tour-chapters-garden-flow.json
Normal 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
|
||||
}
|
||||
]
|
||||
146
source/samford-retreat-garage/tour-chapters-reading.json
Normal file
146
source/samford-retreat-garage/tour-chapters-reading.json
Normal 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
|
||||
}
|
||||
]
|
||||
140
source/samford-retreat-garage/tour-chapters.json
Normal file
140
source/samford-retreat-garage/tour-chapters.json
Normal 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
|
||||
}
|
||||
]
|
||||
19
source/samford-retreat-garage/verify_daily_life.py
Normal file
19
source/samford-retreat-garage/verify_daily_life.py
Normal 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)
|
||||
30
source/samford-retreat-garage/verify_garden_flow.py
Normal file
30
source/samford-retreat-garage/verify_garden_flow.py
Normal 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)
|
||||
Reference in New Issue
Block a user