Add property wind turbine concept and update project gallery

This commit is contained in:
2026-09-24 13:08:12 +10:00
parent cdf0d311ac
commit 2054217c77
19 changed files with 6163 additions and 20 deletions

View File

@@ -0,0 +1,26 @@
# v009 wind turbine concept
`add_wind_turbine.py` is an incremental Blender 5.2.1 script. Use the public
v007 presentation model or the private licensed v008 model as input; do not
run it on a v009 output. It adds the turbine, matching lightweight geometry,
a separate overview camera and tour chapter 32. Existing scenes are preserved.
```sh
blender --background blender/samford-solarpunk-presentation-tour.blend \
--python source/samford-v009-wind/add_wind_turbine.py -- \
--output blender/samford-solarpunk-wind-tour-v009.blend \
--render renders/v009-wind-overview.png
blender --background blender/samford-solarpunk-wind-tour-v009.blend \
--python source/samford-v009-wind/verify_wind_turbine.py
```
Rendering uses Cycles/Metal on macOS; adapt device selection for other hosts.
The verification script reopens the saved file and checks turbine geometry,
viewport copies, retained features, future outlines, material references and
all 32 caption intervals. It writes a sibling `.verification.json` report.
Private v008 inputs must write exclusively to a private directory. Never
publish a `.blend` containing the licensed Tesla. The public render uses the
distributable v007 vehicle. The turbine is original concept geometry, not a
manufacturer model or engineering design. Read the v009 decision for siting
and equipment assumptions. No Unreal assets or existing film were rebuilt.

View File

@@ -0,0 +1,192 @@
"""Run with Blender --background INPUT.blend --python this.py -- --output OUTPUT.blend.
Add an illustrative turbine to either public v007 or private v008. Never publish
an output based on the licensed v008 scene. Dimensions are visual placeholders.
"""
import argparse
import bpy
import json
import math
import sys
from pathlib import Path
from mathutils import Vector
parser = argparse.ArgumentParser()
parser.add_argument('--output', required=True)
parser.add_argument('--render')
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:])
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
s = bpy.context.scene
assert '39 WIND - property turbine concept' not in bpy.data.collections, 'Already added'
detail = bpy.data.collections['DETAIL - full geometry for renders']
fast = bpy.data.collections['00 FAST TOUR - lightweight viewport']
detail.hide_viewport = False
fast.hide_viewport = True
coll = bpy.data.collections.new('39 WIND - property turbine concept')
detail.children.link(coll)
def material(name, color, metallic=0):
m = bpy.data.materials.new(name)
m.diffuse_color = (*color, 1)
m.use_nodes = True
shader = m.node_tree.nodes.get('Principled BSDF')
shader.inputs['Base Color'].default_value = (*color, 1)
shader.inputs['Metallic'].default_value = metallic
shader.inputs['Roughness'].default_value = .38
return m
white = material('WIND - warm ivory composite', (.79, .80, .74))
steel = material('WIND - galvanised mast', (.42, .47, .45), .65)
dark = material('WIND - dark service enclosure', (.095, .14, .12), .35)
concrete = material('WIND - foundation concept', (.37, .35, .29))
def adopt(ob, name, mat):
ob.name = 'WIND - ' + name
for c in list(ob.users_collection):
c.objects.unlink(ob)
coll.objects.link(ob)
ob.data.materials.append(mat)
return ob
def box(name, p, dimensions, mat):
bpy.ops.mesh.primitive_cube_add(size=1, location=p)
ob = adopt(bpy.context.object, name, mat)
ob.scale = dimensions
return ob
def cone(name, a, b, r1, r2, mat):
a, b = Vector(a), Vector(b)
bpy.ops.mesh.primitive_cone_add(vertices=40, radius1=r1, radius2=r2,
depth=(b-a).length, location=(a+b)/2)
ob = adopt(bpy.context.object, name, mat)
ob.rotation_euler = (b-a).to_track_quat('Z', 'Y').to_euler()
for face in ob.data.polygons:
face.use_smooth = True
return ob
x, y = -36., 40.
ground = lambda x, y: -.024*y + .004*x + .12*math.sin(x*.19)*math.cos(y*.14) + .055*math.sin(x*.72+y*.32)
z = ground(x, y)
hub = z + 18.
box('foundation cap - indicative only', (x, y, z+.10), (2.1, 2.1, .30), concrete)
cone('tapered freestanding mast', (x,y,z+.25), (x,y,hub), .27, .13, steel)
cone('base flange', (x,y,z+.23), (x,y,z+.30), .42, .42, steel)
for i in range(8):
angle = i*math.tau/8
bx, by = x+.34*math.cos(angle), y+.34*math.sin(angle)
cone('anchor bolt %02d' % i, (bx,by,z+.3), (bx,by,z+.37), .025, .025, dark)
cone('nacelle', (x,y-.35,hub), (x,y+.7,hub), .24, .18, white)
cone('rotor hub', (x,y-.32,hub), (x,y-.73,hub), .23, .07, white)
cone('tail boom', (x,y+.45,hub), (x,y+2.0,hub), .055, .035, steel)
box('tail vane', (x,y+1.9,hub+.32), (.055,.8,.65), white)
# Three tapered swept blades, with closed airfoil-like cross sections.
for blade in range(3):
angle = blade*math.tau/3
vs, faces = [], []
stations = [(.19,.10,0),(.55,.25,0),(1.1,.23,.04),(2.,.16,.13),(2.8,.08,.22),(3.,.025,.25)]
for r, halfwidth, sweep in stations:
for tangent, depth in [(-halfwidth,0),(0,-.055),(halfwidth,0),(0,.045)]:
t = tangent+sweep
vs.append((x+t*math.cos(angle)+r*math.sin(angle), y-.48+depth,
hub-t*math.sin(angle)+r*math.cos(angle)))
for j in range(len(stations)-1):
for k in range(4):
faces.append((4*j+k,4*j+(k+1)%4,4*(j+1)+(k+1)%4,4*(j+1)+k))
faces.extend([(3,2,1,0),tuple(range(len(vs)-4,len(vs)))])
mesh = bpy.data.meshes.new('WIND blade mesh')
mesh.from_pydata(vs, [], faces)
mesh.update()
ob = bpy.data.objects.new('WIND - rotor blade %d' % (blade+1), mesh)
coll.objects.link(ob)
mesh.materials.append(white)
for face in mesh.polygons:
face.use_smooth = True
box('lockable isolation cabinet - indicative', (x+.72,y,z+.92), (.45,.3,.65), dark)
cone('service conduit', (x+.72,y,z+.1), (x+.72,y,z+.63), .045,.045,steel)
# Exact low-poly display copies keep the lightweight viewport aligned.
bpy.context.view_layer.update()
for ob in list(coll.objects):
proxy = ob.copy()
proxy.data = ob.data.copy()
proxy.name = 'FAST | ' + ob.name
fast.objects.link(proxy)
proxy.hide_render = True
tour = bpy.data.collections['22 PLAY - guided camera tour']
cam = bpy.data.objects['PLAY TOUR camera']
title = '32 / WIND POWER - PROPERTY CONCEPT'
for frame, position in [(3721,(-57,66,25)),(3840,(-54,62,24))]:
s.frame_set(frame)
cam.location = position
cam.rotation_euler = (Vector((x,y,hub*.57))-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 = 38
cam.data.keyframe_insert('lens', frame=frame)
ref = next(o for o in tour.objects if o.type=='FONT' and o.data.body.startswith('31 /'))
ref.scale = (0,0,0)
ref.keyframe_insert('scale', frame=3721)
caption = ref.copy()
caption.data = ref.data.copy()
caption.animation_data_clear()
tour.objects.link(caption)
caption.name = 'Tour caption 32 - wind turbine concept'
caption.data.body = title
caption.location = (-36/38*.91,36/38*9/16*.84,-2)
for frame, value in [(1,0),(3720,0),(3721,1),(3840,1)]:
caption.scale = (value,value,value)
caption.keyframe_insert('scale', frame=frame)
for ob in (ref,caption):
for layer in ob.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for curve in bag.fcurves:
for key in curve.keyframe_points:
key.interpolation = 'CONSTANT'
s.frame_end = 3840
s.timeline_markers.new(title, frame=3721)
s['Wind turbine concept'] = 'v009: three-blade freestanding turbine at (-36,40); 18 m hub / 6 m rotor are visual placeholders. No measured wind resource, selected product, yield, engineering or approvals.'
s['PLAY THE TOUR'] = 'Hover over the 3D view and press Space. 32 chapters, 2 min 40 sec. Shift+Left Arrow restarts. Wind turbine: chapter 32.'
text = bpy.data.texts.get('START HERE - PRESS PLAY')
if text:
text.write('\n\n'+s['PLAY THE TOUR']+'\n'+s['Wind turbine concept'])
# A separate overview camera leaves all existing inspection chapters intact.
camera_data = bpy.data.cameras.new('Wind property overview')
overview = bpy.data.objects.new('Wind property overview', camera_data)
s.collection.objects.link(overview)
overview.location = (-85,90,60)
overview.rotation_euler = (Vector((-5,5,4))-overview.location).to_track_quat('-Z','Y').to_euler()
camera_data.type = 'ORTHO'
camera_data.ortho_scale = 108
camera_data.clip_end = 1000
s.frame_set(3721)
s.camera = cam
detail.hide_viewport = True
fast.hide_viewport = False
s.render.resolution_x, s.render.resolution_y = 2560, 1440
s.render.resolution_percentage = 100
bpy.ops.wm.save_as_mainfile(filepath=str(output))
if args.render:
s.camera = overview
for ob in tour.objects:
if ob.type == 'FONT':
ob.hide_render = True
detail.hide_viewport = False
fast.hide_viewport = True
s.render.engine = 'CYCLES'
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 device in prefs.devices:
device.use = device.type == 'METAL'
s.cycles.device = 'GPU'
s.render.filepath = str(Path(args.render).resolve())
bpy.ops.render.render(write_still=True)
print('WIND_REVISION_SAVED', output, flush=True)

View File

@@ -0,0 +1,19 @@
{
"AGENTS.md": "f20c3f60e3d8d27b5d4d89a99ebff9e8a23499530f836263ece51d9f806a009f",
"README.md": "2d6e86ac8a38799f984bc95e055df7bd1fcc0d58c7078dbf276b2eb9b08c8801",
"docs/PROJECT-CONTEXT.md": "f44ba333fcf5a6955cd35f281c21677e7c732ea67956e52ea643d2e2349bca95",
"docs/REQUEST-HISTORY.md": "555ec31c40f80a04ee0d24db0fcc766768d66e91a7f692a6b70f9eab2c1d28a7",
"decisions/2026-09-24-v009-wind-turbine.md": "26118e95b1fe9f716bb3a7ed6e7df8c31eff2fcb038500f2b4e6c81e1ec5e228",
"blender/samford-solarpunk-wind-tour-v009.blend": "a02c3898da7f2e385a0a57b5610c0b1035997116f747919a46cebfed8cd12c8a",
"blender/samford-solarpunk-wind-tour-v009.verification.json": "48f5a3909abecb42fcde45efc5800b94f749d00902af2e973d87c10db298f3af",
"blender/wind-turbine-layout-v009.json": "3e9d06c55ff24bdce810b33d1942fde9f082cf8bd24b46f24126abc3000e4dab",
"blender/tour-chapters-wind-v009.json": "378b08b048d8c480772c76bc7a141cbede17c531b919658130b033edacd2d735",
"plans/concept/wind-turbine-plan-v009.svg": "d4e7cf9aad22559f61f123f37775315934a06f3769198099062a756f4c379107",
"renders/v009-wind-overview.png": "1aa727e37a263eed860718c758624af416f0a094aac7ca6368e4a555851b60e8",
"site/samford-valley-home/index.html": "7cbd30f42a4e23a5d9d5bdb939c1beec20c5dd9d8dfe697e607b8099e9d3bade",
"site/metadata/sitemap.xml": "6af5dec5ef6d887c751becc9549970e7251fce6e164037f3d80da02ecda63c27",
"site/metadata/llms.txt": "5810cb29ffa7f9b1521babb3cedf1a7b8a95cd5af5f66e3072ef466489cdc9ed",
"source/samford-v009-wind/add_wind_turbine.py": "c1e9b7112473b888d6f5d34c137f8e166be7e4dc0679d2e0074f37a822b326cb",
"source/samford-v009-wind/verify_wind_turbine.py": "ef54260f1c66079f59af06a551bf077f14af1fa5da3311eefcb83297e5b58c71",
"source/samford-v009-wind/README.md": "fc9f6f3f18686445fe270808bf3a4db479f8b7292168ea11a4b626ed63835fb0"
}

View File

@@ -0,0 +1,42 @@
"""Reopen either saved v009 scene and validate geometry, preservation and tour."""
import bpy
import json
from pathlib import Path
s = bpy.context.scene
detail = bpy.data.collections['DETAIL - full geometry for renders']
detail.hide_viewport = False
bpy.context.view_layer.update()
objects = bpy.data.collections['39 WIND - property turbine concept'].objects
assert len([o for o in objects if o.name.startswith('WIND - rotor blade')]) == 3
mast = bpy.data.objects['WIND - tapered freestanding mast']
assert abs(mast.location.x + 36) < .001 and abs(mast.location.y - 40) < .001
assert abs(mast.dimensions.z - 17.75) < .001
assert all('FAST | '+o.name in bpy.data.objects for o in objects)
assert all(bpy.data.objects['FAST | '+o.name].hide_render for o in objects)
assert len(bpy.data.collections['24 LATER - Shadow spaces only'].objects) == 26
required = ['ISLAND - honed marble countertop', 'FAST | Garden Buddha statue',
'CONTAINER WATER - steel base frame', 'Living woven jute rug',
'Fuller garden borders - curved lanceolate leaves']
assert all(name in bpy.data.objects for name in required)
assert not any('table tennis' in o.name.lower() for o in s.objects)
assert all(m in bpy.data.materials.values() for o in s.objects if o.type=='MESH' for m in o.data.materials if m)
fonts = [o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
captions = []
for frame in range(1,3841,120):
s.frame_set(frame+20)
visible = [o.data.body for o in fonts if o.scale.length > .5]
assert len(visible)==1, (frame,visible)
captions.extend(visible)
assert captions[-1].startswith('32 / WIND') and captions[-2].startswith('31 /')
assert s.frame_end == 3840
assert s.camera.name == 'PLAY TOUR camera'
report = dict(status='PASS', model=Path(bpy.data.filepath).name,
turbine_objects=len(objects), rotor_blades=3, hub_height_m=18,
rotor_diameter_m=6, concept_xy=[-36,40], tour_chapters=32,
tour_seconds=160, captions=captions, preserved_features=required,
future_outline_objects=26, material_references_valid=True,
dimensions_status='Illustrative placeholders, not engineered')
target = Path(bpy.data.filepath).with_suffix('.verification.json')
target.write_text(json.dumps(report,indent=2)+'\n')
print('WIND_VERIFIED',target,flush=True)