Files
Emil 89c53d7058 Expand material support and checkpoint the completed building toolkit
Expose the runtime block/item registry through compact material search and
single-material descriptions. Preserve private block-entity data through
checked edits and durable undo without sending payloads to model context.

Include the completed terrain tools, isolated world/map plugin, station lift,
ACP streaming and guidance fixes, local camera auto-connect, construction
scripts, and their public documentation, references and verification records.
Active station decoration and private runtime data remain outside this commit.

Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build,
and isolated Paper verification of all 1,196 block defaults plus 5,392
independent property cases for placement, same-material edits and restoration.
2026-09-13 16:59:13 +03:00

277 lines
12 KiB
Python

#!/usr/bin/env python3
"""Rasterize the reviewed Shacraft stage-one design without editing the world.
The public build_geometry(design, layout) function uses only the Python standard
library. Coordinate keys are (x, z). Each cell has block_y, kind, group and clear;
straight bottom stairs additionally have facing. block_y is not player feet Y.
Precedence: exact foundation polygons, northeast landing, local roads, central
station staircase. Existing dense approved centerlines are used for road curves.
Roads extend a few flat rows back into their origin plaza so an avenue cannot
pinch down to the one-block vertex of the hexagonal square.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
Point = tuple[int, int]
Cell = dict[str, Any]
def _on_segment(x: int, z: int, a, b) -> bool:
cross = (x-a[0])*(b[1]-a[1]) - (z-a[1])*(b[0]-a[0])
return abs(cross) < 1e-8 and min(a[0],b[0]) <= x <= max(a[0],b[0]) and min(a[1],b[1]) <= z <= max(a[1],b[1])
def polygon_cells(vertices) -> set[Point]:
"""Integer block columns inside OR on the exact polygon, with no box fill."""
vertices = [tuple(p) for p in vertices]
if len(vertices) < 3:
raise ValueError('A foundation polygon needs three vertices')
segments = list(zip(vertices, vertices[1:]+vertices[:1]))
result: set[Point] = set()
for z in range(math.floor(min(p[1] for p in vertices)), math.ceil(max(p[1] for p in vertices))+1):
for x in range(math.floor(min(p[0] for p in vertices)), math.ceil(max(p[0] for p in vertices))+1):
inside = False
boundary = False
for a,b in segments:
if _on_segment(x,z,a,b):
boundary = True
break
if (a[1] > z) != (b[1] > z):
cross_x = a[0] + (z-a[1])*(b[0]-a[0])/(b[1]-a[1])
if x < cross_x:
inside = not inside
if boundary or inside:
result.add((x,z))
return result
def _distance_squared(x: int, z: int, a, b) -> float:
dx,dz=b[0]-a[0],b[1]-a[1]
if not dx and not dz:
return (x-a[0])**2+(z-a[1])**2
t=max(0.,min(1.,((x-a[0])*dx+(z-a[1])*dz)/(dx*dx+dz*dz)))
return (x-a[0]-t*dx)**2+(z-a[1]-t*dz)**2
def _densify(points) -> list[Point]:
result=[]
for a,b in zip(points,points[1:]):
n=max(1,math.ceil(max(abs(a[0]-b[0]),abs(a[1]-b[1]))))
for i in range(n+1):
p=(round(a[0]+(b[0]-a[0])*i/n),round(a[1]+(b[1]-a[1])*i/n))
if not result or result[-1] != p:
result.append(p)
if not result and points:
result=[tuple(map(round,points[0]))]
return result
def _cardinal_path(points: list[Point]) -> list[Point]:
"""Insert cardinal intermediate samples for collision checks on diagonals."""
if not points:
return []
result=[points[0]]
for x,z in points[1:]:
ax,az=result[-1]
while (ax,az)!=(x,z):
if ax!=x:
ax += 1 if x>ax else -1
else:
az += 1 if z>az else -1
result.append((ax,az))
return result
def _approved_centerline(spec, layout) -> list[Point]:
base_id=spec['id'].removesuffix('-local')
approved=next((r for r in layout.get('routes',[]) if r['id']==base_id),None)
points=_densify(approved['points'] if approved else spec['waypoints'])
if spec['geometry']=='road_profile':
axis=0 if spec['axis']=='x' else 1
lo=min(row['coordinate'] for row in spec['rows'])
hi=max(row['coordinate'] for row in spec['rows'])
points=[p for p in points if lo <= p[axis] <= hi]
# If the approved path terminates just before a reviewed local endpoint,
# add the small explicit tail without replacing its established curve.
expected=tuple(spec['waypoints'][-1])
if points and points[-1][axis] != expected[axis]:
points += _densify([points[-1],expected])[1:]
elif 'profile_until_z' in spec:
stop=spec['profile_until_z']
points=[p for p in points if p[1]>=stop]
if len(points)<2:
raise ValueError(f"No usable centerline for {spec['id']}")
return points
def _origin_extension(points: list[Point], spec) -> list[Point]:
"""Overlap the origin plateau without changing its level or approved curve."""
first=points[0]
distance=max(4,spec['width']//2+2)
target=points[min(len(points)-1,distance)]
dx,dz=target[0]-first[0],target[1]-first[1]
length=math.hypot(dx,dz)
if not length:
return points
back=(round(first[0]-dx*distance/length),round(first[1]-dz*distance/length))
return _densify([back,first])[:-1]+points
def _corridor(points: list[Point], width: int, axis=None, lo=None, hi=None):
"""Clear corridor plus one block of side coping, evaluated by cell centers."""
outer=width/2+1
clear_radius2=(width/2)**2
outer_radius2=outer**2
distances: dict[Point,float] = {}
for a,b in zip(points,points[1:]):
for z in range(math.floor(min(a[1],b[1])-outer),math.ceil(max(a[1],b[1])+outer)+1):
for x in range(math.floor(min(a[0],b[0])-outer),math.ceil(max(a[0],b[0])+outer)+1):
if axis is not None and not lo <= (x,z)[axis] <= hi:
continue
d=_distance_squared(x,z,a,b)
if d <= outer_radius2+1e-8 and d < distances.get((x,z),math.inf):
distances[(x,z)]=d
return {p:d<=clear_radius2+1e-8 for p,d in distances.items()}
def tread_height(cell: Cell, local_x: float, local_z: float) -> float:
"""Collision surface of a full block or a straight bottom stair at an offset.
Use .25/.75 offsets to inspect both tread halves, avoiding the central edge.
"""
if cell['kind']=='full':
return cell['block_y']+1
high={'north':local_z<.5,'south':local_z>.5,
'west':local_x<.5,'east':local_x>.5}[cell['facing']]
return cell['block_y']+(1. if high else .5)
def build_geometry(design, layout):
"""Return cells, route metadata and compact invariant checks.
cells[(x,z)] -> {block_y:int,kind:'full'|'stairs',facing?:str,
group:str,clear:bool}
routes[] -> {id,centerline,centerline_4,corridor,clear_cells,clear_width}
`clear` distinguishes usable road paving from exterior coping; for a main
polygon every floor cell is clear. It does not mean the world has been cleared.
"""
cells: dict[Point,Cell] = {}
routes=[]
polygon_areas={}
for surface in design['surfaces']:
footprint=polygon_cells(surface['points'])
polygon_areas[surface['id']]=len(footprint)
for p in footprint:
cells[p]={'block_y':surface['floor_y'],'kind':'full','group':surface['id'],'clear':True}
landing=design.get('station_ne_connection')
if landing:
for p in polygon_cells(landing['points']):
cells[p]={'block_y':landing['floor_y'],'kind':'full','group':'station-ne-connection','clear':True}
for spec in design['roads']:
centerline=_approved_centerline(spec,layout)
extended=_origin_extension(centerline,spec)
profiled=spec['geometry']=='road_profile'
if profiled:
axis=0 if spec['axis']=='x' else 1
profiles={row['coordinate']:row for row in spec['rows']}
# Extend only the origin; stop exactly at the designed last road row.
endpoint=centerline[-1][axis]
origin=extended[0][axis]
lo,hi=sorted([endpoint,origin])
corridor=_corridor(extended,spec['width'],axis,lo,hi)
first=spec['rows'][0]
else:
corridor=_corridor(extended,spec['width'])
# The last flat avenue rows must not cover the station staircase.
if 'profile_until_z' in spec:
corridor={p:clear for p,clear in corridor.items() if p[1]>=spec['profile_until_z']}
for p,clear in corridor.items():
if profiled:
row=profiles.get(p[axis])
if row is None:
row={'block_y':first['block_y'],'kind':'full'}
cell={'block_y':row['block_y'],'kind':row['kind'],'group':spec['id'],'clear':clear}
if row['kind']=='stairs':
cell['facing']=row['facing']
else:
cell={'block_y':spec['floor_y'],'kind':'full','group':spec['id'],'clear':clear}
# A coping line within an already level clear plaza is a floor band,
# not an obstacle or a place for a parapet. Preserve that distinction.
previous=cells.get(p)
if previous and previous['clear'] and previous['block_y']==cell['block_y'] and previous['kind']==cell['kind']=='full':
cell['clear']=True
cells[p]=cell
routes.append({'id':spec['id'],'centerline':centerline,'centerline_4':_cardinal_path(centerline),'corridor':sorted(corridor),'clear_cells':sorted(p for p,c in corridor.items() if c),'clear_width':spec['width']})
stair=design['station_entrance_stair']
stair_cells=[]
stair_clear=[]
for row in stair['rows']:
for x in range(stair['x_min']-1,stair['x_max']+2):
p=(x,row['z'])
clear=stair['x_min']<=x<=stair['x_max']
cells[p]={'block_y':row['block_y'],'kind':row['kind'],'group':'station-entrance-stair','clear':clear}
if row['kind']=='stairs':
cells[p]['facing']=row['facing']
stair_cells.append(p)
if clear:
stair_clear.append(p)
mid=(stair['x_min']+stair['x_max'])//2
line=[(mid,row['z']) for row in stair['rows']]
routes.append({'id':'station-entrance-stair','centerline':line,'centerline_4':_cardinal_path(line),'corridor':stair_cells,'clear_cells':stair_clear,'clear_width':stair['width']})
missing=[]
blocked=[]
for route in routes:
for p in route['centerline_4']:
if p not in cells:
missing.append((route['id'],p))
elif not cells[p]['clear']:
blocked.append((route['id'],p))
if missing or blocked:
raise ValueError(f'Road centerline incomplete: missing={missing[:8]}, non-clear={blocked[:8]}')
max_step=0.
for route in routes:
path=route['centerline_4']
route_step=0.
for p,q in zip(path,path[1:]):
dx,dz=q[0]-p[0],q[1]-p[1]
departure=tread_height(cells[p],.5+dx*.25,.5+dz*.25)
arrival=tread_height(cells[q],.5-dx*.25,.5-dz*.25)
step=abs(departure-arrival)
if step>.5:
raise ValueError(f"Unwalkable centerline in {route['id']}: {p}->{q}, {step} blocks")
route_step=max(route_step,step)
route['maximum_centerline_step']=route_step
max_step=max(max_step,route_step)
counts={}
for cell in cells.values():
counts[cell['group']]=counts.get(cell['group'],0)+1
return {'cells':cells,'routes':routes,'checks':{'columns':len(cells),'polygon_areas':polygon_areas,'columns_by_final_group':counts,'centerline_missing':len(missing),'centerline_nonclear':len(blocked),'maximum_centerline_step':max_step,'stairs':sum(c['kind']=='stairs' for c in cells.values()),'minimum_block_y':min(c['block_y'] for c in cells.values()),'maximum_block_y':max(c['block_y'] for c in cells.values()),'world_edits':0}}
def serializable(geometry):
return {**geometry,'cells':[{'x':x,'z':z,**cell} for (x,z),cell in sorted(geometry['cells'].items(),key=lambda p:(p[0][1],p[0][0]))]}
def main():
root=Path(__file__).resolve().parents[2]
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--design',type=Path,default=root/'.runtime/foundation-study/design/foundation-design.json')
parser.add_argument('--layout',type=Path,default=root/'examples/layout/shacraft-lobby-layout.json')
parser.add_argument('--output',type=Path,default=root/'.runtime/foundation-study/design/geometry.json')
args=parser.parse_args()
geometry=build_geometry(json.loads(args.design.read_text()),json.loads(args.layout.read_text()))
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(serializable(geometry),separators=(',',':'))+'\n')
print(json.dumps({'output':str(args.output),'checks':geometry['checks']},indent=2))
if __name__=='__main__':
main()