Buckets:
| """A brush-width-aware Eiffel Tower and Seine stroke plan. | |
| Coordinates are normalized canvas coordinates: u=0 is the left edge and v=0 | |
| is the top edge. These are desired strokes, not observations or a claim that | |
| the simulator has painted them. The default plan has 19 strokes. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from pathlib import Path | |
| COLORS = { | |
| "brown": (112, 73, 47), | |
| "blue": (35, 112, 201), | |
| "green": (39, 148, 93), | |
| "orange": (240, 140, 48), | |
| } | |
| # New versions should be added after inspecting an actual camera result. | |
| # Version 1 is a credible initial design, not an intentionally weak baseline. | |
| VERSIONS = { | |
| 1: {"tower_width": .40, "crossbrace": True, "detail": 1, "river_spacing": .044}, | |
| } | |
| def _linspace(start, stop, count): | |
| return [start + (stop-start)*i/(count-1) for i in range(count)] | |
| def _quadratic(start, control, end, count=13): | |
| return [[(1-t)**2*start[axis] + 2*(1-t)*t*control[axis] + t*t*end[axis] | |
| for axis in (0, 1)] for t in _linspace(0, 1, count)] | |
| def design(version=1, *, tower_width=None, crossbrace=None, detail=None, river_spacing=None): | |
| """Return [{color, label, points:[[u,v], ...]}, ...] for physical strokes. | |
| ``tower_width`` is the normalized width between the outer tower feet | |
| (default .40). ``crossbrace`` enables the large middle X. ``detail`` is | |
| 0 for that X only, or 1 to include a second, smaller upper X. A larger | |
| ``river_spacing`` separates the three blue curves. All overrides are | |
| explicit; the function does not infer improvements from unseen results. | |
| Use one continuous contact for each list of points. Lift between strokes. | |
| Reload brown paint as needed during the tower; the planner does not read | |
| paint load or any other simulator state. | |
| """ | |
| if version not in VERSIONS: | |
| raise ValueError(f"Unknown design version {version!r}; available: {tuple(VERSIONS)}") | |
| options = dict(VERSIONS[version]) | |
| for name, value in (("tower_width", tower_width), ("crossbrace", crossbrace), | |
| ("detail", detail), ("river_spacing", river_spacing)): | |
| if value is not None: | |
| options[name] = value | |
| width = float(options["tower_width"]) | |
| spacing = float(options["river_spacing"]) | |
| if not .34 <= width <= .50: | |
| raise ValueError("tower_width must be between .34 and .50") | |
| if not .034 <= spacing <= .057: | |
| raise ValueError("river_spacing must be between .034 and .057") | |
| if options["detail"] not in (0, 1): | |
| raise ValueError("detail must be 0 or 1") | |
| strokes = [] | |
| def stroke(color, label, points): | |
| points = [[round(float(u), 6), round(float(v), 6)] for u, v in points] | |
| if len(points) < 2 or any(not (0 <= u <= 1 and 0 <= v <= 1) for u, v in points): | |
| raise ValueError(f"Invalid stroke {label}") | |
| strokes.append({"color": color, "label": label, "points": points}) | |
| def tower(label, points): | |
| # Construction coordinates use an outer-foot span of .43. | |
| stroke("brown", label, [[.5 + (u-.5)*width/.43, v] for u, v in points]) | |
| # Curved, flaring legs and a short antenna are the primary silhouette. | |
| outline = [[.5,.205], [.478,.305], [.450,.404], [.407,.510], [.350,.604], [.285,.690]] | |
| tower("tower-left-outline", list(reversed(outline))) | |
| tower("tower-right-outline", [[1-u,v] for u,v in outline]) | |
| tower("tower-antenna", [[.5,.120],[.5,.217]]) | |
| # The observation decks extend past the sloping legs, like the real tower. | |
| tower("tower-lower-deck", [[.351,.550],[.649,.550]]) | |
| tower("tower-middle-deck", [[.429,.415],[.571,.415]]) | |
| tower("tower-upper-deck", [[.462,.285],[.538,.285]]) | |
| # Leave a large white opening below the lower deck. The broad arch reads | |
| # clearly with a 3.6-5.5mm brush; small lattice details would merge here. | |
| arch = _quadratic([.365,.690],[.393,.593],[.5,.590]) | |
| arch += _quadratic([.5,.590],[.607,.593],[.635,.690])[1:] | |
| tower("tower-base-arch", arch) | |
| tower("tower-left-foot", [[.268,.695],[.394,.695]]) | |
| tower("tower-right-foot", [[.606,.695],[.732,.695]]) | |
| if options["crossbrace"]: | |
| tower("tower-middle-brace-rising", [[.401,.529],[.557,.434]]) | |
| tower("tower-middle-brace-falling", [[.443,.434],[.599,.529]]) | |
| if options["detail"]: | |
| tower("tower-upper-brace-rising", [[.451,.394],[.526,.319]]) | |
| tower("tower-upper-brace-falling", [[.474,.319],[.549,.394]]) | |
| # A single spiral fills the sun by overlapping real brush passes. | |
| sun = [] | |
| for t in _linspace(0, 1, 68): | |
| angle = t*math.tau*2.4 | |
| radius = .060*t | |
| sun.append([.790 + radius*math.cos(angle), .218 + .9*radius*math.sin(angle)]) | |
| stroke("orange", "sun", sun) | |
| # Two disconnected banks keep the tower base open and uncluttered. | |
| stroke("green", "left-river-bank", _quadratic([.100,.754],[.225,.710],[.352,.748])) | |
| stroke("green", "right-river-bank", _quadratic([.654,.748],[.770,.714],[.900,.750])) | |
| # Three separated, gently curving blue strokes suggest a broad river and | |
| # reflected light. The lowest remains safely inside the paper margin. | |
| for index in range(3): | |
| river = [[u, .795 + index*spacing + .020*math.sin((u-.10)/.80*math.tau-.5)] | |
| for u in _linspace(.105,.895,31)] | |
| stroke("blue", f"seine-curve-{index+1}", river) | |
| return strokes | |
| def estimated_seconds(strokes=None, *, speed=.035, reloads=6): | |
| """Planning estimate for the current controller; not measured runtime. | |
| Uses a 135x150mm canvas, 2.45s approach/touch/lift per stroke, a minimum | |
| .8s contact, and 3.8s per palette visit. It excludes calibration and video | |
| holds. Six visits allow three brown reloads plus orange, green, and blue. | |
| """ | |
| if strokes is None: | |
| strokes = design() | |
| seconds = 3.8*reloads | |
| for item in strokes: | |
| length = sum(math.hypot((b[0]-a[0])*.135, (b[1]-a[1])*.150) | |
| for a,b in zip(item["points"],item["points"][1:])) | |
| seconds += 2.45 + max(.8,length/speed) | |
| return seconds | |
| def render_target(path, version=1, *, size=800, brush_width_mm=4.5, **overrides): | |
| """Save an explicitly labeled reference PNG, not simulated brush output. | |
| The drawing panel preserves the real canvas's 135:150 aspect ratio. | |
| The solid lines indicate intended paths and nominal width; actual paint | |
| appearance depends on physical contact, pigment load, and camera lighting. | |
| """ | |
| from PIL import Image, ImageDraw, ImageFont | |
| strokes = design(version, **overrides) | |
| height, width = int(size), round(size*.135/.150) | |
| header, footer = 68, 42 | |
| image = Image.new("RGB", (width,height+header+footer), (241,239,233)) | |
| draw = ImageDraw.Draw(image) | |
| title_font = ImageFont.load_default(size=max(14,round(size*.024))) | |
| note_font = ImageFont.load_default(size=max(11,round(size*.016))) | |
| draw.text((18,12), "EIFFEL TOWER + SEINE", fill=(55,51,44), font=title_font) | |
| draw.text((18,40), f"TARGET PLAN v{version} / REFERENCE ONLY", fill=(97,90,77), font=note_font) | |
| draw.rectangle((0,header,width-1,header+height-1), fill=(255,253,247)) | |
| line_width = max(1,round(brush_width_mm/150*height)) | |
| for item in strokes: | |
| points = [(round(u*(width-1)), header+round(v*(height-1))) for u,v in item["points"]] | |
| color = COLORS[item["color"]] | |
| draw.line(points, fill=color, width=line_width, joint="curve") | |
| radius = line_width/2 | |
| for x,y in (points[0],points[-1]): | |
| draw.ellipse((x-radius,y-radius,x+radius,y+radius), fill=color) | |
| draw.text((18,header+height+12), "Desired strokes - not a simulator result or camera image.", | |
| fill=(97,90,77), font=note_font) | |
| destination = Path(path) | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| image.save(destination) | |
| return destination | |
| if __name__ == "__main__": | |
| import argparse | |
| import json | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--render", type=Path, help="Write a labeled target-plan PNG.") | |
| args = parser.parse_args() | |
| if args.render: | |
| print(render_target(args.render)) | |
| print(json.dumps({"strokes":len(design()), "estimated_sim_seconds":round(estimated_seconds(),1)})) | |
Xet Storage Details
- Size:
- 8.28 kB
- Xet hash:
- 693ef746d8d06e3a5d2dbc87afca2c551df43df1b89a0e5b0e387e7010d21d8e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.