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.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user