Add property wind turbine concept and update project gallery
This commit is contained in:
26
source/samford-v009-wind/README.md
Normal file
26
source/samford-v009-wind/README.md
Normal 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.
|
||||
192
source/samford-v009-wind/add_wind_turbine.py
Normal file
192
source/samford-v009-wind/add_wind_turbine.py
Normal 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)
|
||||
19
source/samford-v009-wind/publication-sha256.json
Normal file
19
source/samford-v009-wind/publication-sha256.json
Normal 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"
|
||||
}
|
||||
42
source/samford-v009-wind/verify_wind_turbine.py
Normal file
42
source/samford-v009-wind/verify_wind_turbine.py
Normal 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)
|
||||
Reference in New Issue
Block a user