polyweave logo
Design stage · eight blocks, 36 lines · nothing shipped

Say what the asset has to be.
Let it find the numbers.

polyweave is a Claude Code plugin for making 3D assets — geometry, surface and motion. You state what an asset has to satisfy; it searches for the parameters that satisfy it, measuring against your own reference rather than against an opinion. On top of Blender, Godot and a generative mesh service, on your machine, against files in your own repository.

What driving it looks like
a Claude Code session
# the agent has a drawing and a name, and no numbers at all

> polyweave accept mascot --from docs/design/art/ui/mascot.png
  wrote assets/mascot.accept.toml    3 predicates, 2 parameters to search

> polyweave search mascot
  job j_7f3a  stage=queued
  job j_7f3a  stage=rendering  rung=sphere   18/64  elapsed_s=54
  job j_7f3a  stage=rendering  rung=preview  61/64  elapsed_s=212
  done  score 0.94   light=2.8  form=1.9
        sheet  .polyweave/sheets/mascot-7f3a.png   (5 candidates)
        trace  .polyweave/traces/mascot-7f3a.json  (64 samples, 41 cached)

> polyweave bake mascot --rung final
  done  1024x1024  saturation_p99=0.87  delta_e=1.4  silhouette_iou=0.981
        accept: 3/3 predicates pass   rung=final

> polyweave bake shelf --rung final
  post.render-uniform  the render is a single colour, so nothing was lit
  -> the model 'shelf' carries no material. Set `material` on it, or pass
     `glaze: {roughness: 0.22}` to apply the default.

Four calls: turn a drawing into a spec, search for the parameters that satisfy it, take the verdict at the final rung, and — on the last one — get a failure that names the door instead of a traceback. The commands are the surface docs/specs/tool-surface.md fixes; the values are illustrative.

No GUI and no accountNothing leaves your machineIt never spends money on its own judgement
Blender for the bakeGodot for the worldA generative service for the meshTOML for what a person writes, JSON for what a machine does
Read this first

There is no code yet.

polyweave is at design stage. What this page describes is the product docs/ROADMAP.md and docs/specs/ specify: 36 lines across eight blocks, none of them shipped.

Every measurement quoted here was taken in Cottony, the game the backlog was drawn from, and describes the cost polyweave exists to remove. None of it is a benchmark of polyweave, because there is nothing yet to benchmark — and the line that has to ship before any of this becomes a measurement is on the roadmap too, at the very end.

Where the evidence comes from

Every one of the 36 lines is a failure somebody measured.

Cottony is a real game, and it is where nearly every symptom in this backlog was measured: a fourteen-field render rig found by hand at two minutes a sample, a silhouette bought for thirty credits, a percentile that disagreed with a mean, and two runs of one unchanged scene that differed in 29,696 pixels. The backlog was not imagined and then justified — it was read off a project that already hurt.

36
lines, each with its measurement
Eight
blocks, grouped by the failure they share
0
shipped, because this is a plan and not a product
The cost today

Generating a mesh is the cheap part.

What is expensive is everything between having a mesh and knowing it is right — and today that is a person changing one number at a time and looking at a two-minute render. Six measurements from Cottony, each of which put a line on the backlog.

Fourteen constants, at two minutes a sample

Cottony's render rig has fourteen tuned fields, and every one of them was found by rendering, looking, and changing it by hand. It is the single largest cost in making an asset, and it is the cost this plugin exists to remove.

📉

0.31 against 0.32, and plainly wrong

A board matched the concept art's mean saturation to within 0.01 and still looked washed out. The whole difference sat at the 99th percentile, which is why a measurement here is a distribution and never a single number.

💳

Thirty credits for a silhouette

A wide low cap was sent to the generative service and a tall dome on a long stem came back. A silhouette check against the drawing would have said so before a credit moved.

🎲

29,696 pixels, none by more than 1/255

Two runs of one unchanged scene. A path-traced bake is not byte-reproducible, so equality is the wrong question and a tolerance is the right one — and any usable distance metric has to sit above that noise floor.

🔇

A success returned over an empty mesh

Blender's EXACT boolean returns an empty mesh with no error when its target was bevelled. The silence cost a full render to locate, which is why every operation here asserts its own output before returning.

📖

Fourteen fields, documented as comments

The rig's parameters live in a thousand-line module, so every caller pays a file read to find out what it may set. A surface an agent has to read the implementation to use is one it will get wrong on the first call.

The mechanism

Declare it, then search for it.

One file beside the asset says what makes a render of it correct. A search reads it, proposes values for the parameters the file permits, bakes at the cheapest rung that can answer, measures the result and scores how comfortably each predicate passed. What comes back is the winning values, a contact sheet and a trace of everything it rejected.

WHAT YOU DECLARE mascot.accept.toml silhouette_iou min = 0.97 delta_e max = 2.0 [search.light] 0.5-4.0 THE SEARCH propose values bake at the cheapest rung measure score the margin per predicate, 0 to 1 WHAT COMES BACK the winning values a contact sheet the best handful, side by side a trace every sample it rejected, and the predicate that rejected it cache key the inputs, the renderer and its version, the seed — a sweep revisits neighbourhoods, and a state already rendered is not paid for twice.
  • A bound, not an expression. min, max and target are the only comparisons a predicate has. Anything needing more is a measure that does not exist yet, and the honest response is to add the measure rather than widen the grammar.
  • A margin, not a verdict. Each predicate yields pass/fail and a value in [0, 1] for how comfortably it passed. A pure boolean gives a search a cliff and nothing to climb; the margin is what makes it converge on something rather than wander.
  • The spec is the whole permission. A parameter [search.<param>] does not name is not searched, whatever the optimiser would like. It can tune the exposure; it cannot decide the asset should be twice as large.
  • Every predicate has a name. The trace addresses them by id, so a result can be argued with. An anonymous predicate is one nobody can discuss.
  • Cancelling is not advisory. A search that has found its answer stops paying for the renders it no longer needs, which is what makes running one affordable at all.
  • Nothing is rendered twice. The cache is keyed on the inputs, the renderer and its version, and the seed — a sweep revisits neighbourhoods, and without that key the same picture is paid for as many times as the search returns to it.
THE BEST FIVE, SIDE BY SIDE 0.94 kept light 2.8 · form 1.9 0.71 delta_e 4.6 > 2.0 0.63 silhouette_iou 0.88 0.52 saturation_p99 0.61 0.49 delta_e 7.1 > 2.0 A search that reports only its winner cannot be overruled. If the one a person would have chosen is not the one that scored highest, the spec is what is wrong — and finding that out is the fastest thing this loop does.
The contact sheet, and why it is not a nicety: the expected case is a search that satisfies the spec and returns a render a person rejects. That is the moment you find out the spec is incomplete rather than the renderer — and it is the fastest way this loop earns its keep.
Before you pay for a render

Three seconds, not two minutes.

A surface is read on a sphere, and a sphere renders in three seconds. Nothing in the old pipeline made the cheap look the default, so the expensive one is what got run — every time, for every judgement, including the ones a sphere could have settled.

rung: sphere 3 s Does the surface read? Roughness, glaze, colour. No mesh needed. rung: preview low samples, real mesh Does the silhouette land? Mass, proportion, where the shape sits. rung: final 2 min The verdict rung. What a spec may be accepted at, and nothing below. The rung comes back with every measurement, reported and never inferred, so a verdict taken on a sphere is never mistaken for one taken on the final mesh. The three seconds and the two minutes are Cottony's, measured before any of this existed.
  • The picture and its numbers are one answer. A render returns the image alongside its measurements, so a verdict costs one turn rather than three.
  • The region is masked, and named. subject means the pixels where alpha clears the floor, so a prop is measured over its own pixels and not diluted by whatever background it happens to sit on.
  • At the size it will be seen. Cottony's fluff read at 1.2 and vanished at 1.0, at a third of the sprite's authored size. luma_bands takes the display size and downscales before counting, which is the only honest way to ask that question.
  • Beside its siblings, not alone. A prop that reads correctly on its own can be the one thing on a sheet with no shadow and no specular window, which no solo render shows.

A closed vocabulary, so a spec is checkable

A measure named in an acceptance spec has to be one of these. An unknown name is a refusal — spec.unknown-measure — never a warning, because a spec that accepts any string is a spec that silently checks nothing.

MeasureRangeWhat it is
saturation_{p1,p50,p99,mean,std}0–1HSL saturation over the region, returned as a set and never as one number
luma_{p1,p50,p99,mean,std}0–1Relative luminance, sRGB-weighted
hue_spread0–1Circular standard deviation of hue, weighted by saturation
alpha_coverage0–1Fraction of the region above the alpha floor
silhouette_iou0–1Intersection over union of the alpha mask against a reference drawing
silhouette_centroid_offsetpxDistance between the two mask centroids
silhouette_bbox_deltapxLargest per-edge difference between the bounding boxes
region_colourLabMean CIELAB colour over the region
delta_e0–100CIEDE2000 distance to a target: under 2 is a difference you have to look for
distance0–1Perceptual distance between two renders, above the sampler's own noise
changed_fraction0–1Fraction of pixels differing by more than a stated amount
luma_bandscountDistinct luminance bands surviving a downscale to the size it will be seen at
What a call costs the turn

One call, right the first time.

One measure governs the whole surface: a caller gets a call right on the first attempt, and answers a failure, without opening an implementation file. Where a rule does not serve that, the rule is wrong.

  • Anything over a couple of seconds returns a handle. A bake, a fetch, a capture and a search all do; a measurement and a config read do not. stage is a short vocabulary, so a caller branches on a word rather than parsing a message.
  • The handle survives the session. Job state is a file in .polyweave/jobs/, so a session that ends mid-render can be told what happened by the next one.
  • A dead worker is a failure, not a hang. The record carries the OS process id; a poll that finds no live process and no result returns job.worker-gone.
  • An unknown field is refused, never dropped. A silent drop makes a typo indistinguishable from a working call, which is how a payload that validates comes to prove nothing.
  • The surface describes itself. describe() returns each parameter's type, range, default and one sentence, read from the implementation, so the documentation and the code cannot drift.
  • And it describes this machine. capabilities() says which renderer and version are here, whether an engine is reachable, which offscreen route works, whether a service key is present and what budget is left — so a caller plans against it rather than discovering a missing binary three calls later.

Every operation asserts its own output

A success returned over a result nobody checked is a failure this plugin exists partly to remove. Before returning, each operation checks what must be true of what it produced. A failed assertion is an error, never a warning, and its code names the assertion.

ProducesAsserted before it returns
A meshat least one face; finite bounds; no NaN in any vertex
A boolean resultface count is not zero where both operands had faces
A rendernot a single uniform colour; not fully transparent; dimensions as requested
A texturenot fully transparent; not a single uniform colour
A downloadbyte length matches the declared length; sha256 recorded
A capturethe named artefact exists on disk and is a readable image

An error names the door

A traceback says where the code gave up, not what the caller should do instead — which is the one thing needed to retry without another round trip.

{
  "code": "render.no-material",
  "message": "the model 'mascot' carries no material, so the rig has nothing to light",
  "remedy": "set `material` on the model, or pass `glaze: {roughness: 0.22}` to apply the default",
  "detail": "…traceback, for a human…"
}

code is stable and namespaced by area, and does not change once published. remedy is the call that closes it, with the arguments filled in wherever they are derivable; where the choice is a judgement the tool cannot make, it names both doors and what separates them. message never contains a traceback. detail may.

Someone else's balance

Bounded, visible, and never the agent's call.

A fetch draws on a real balance. The ceiling is a person's to set; the plugin's job is to make a spend bounded and visible, and never to decide one is worth it. That is a constraint on this project, not a feature of it.

the request a prompt, and the drawing it must match THE GATE silhouette_iou against the reference, before a credit moves a refusal costs nothing fetch.silhouette-mismatch a tall dome for a wide low cap [budget] a ceiling, agreed once not a question per fetch, and never the agent's call spent 18 / 200 KEPT the mesh, on your disk sha256 · prompt · seed service · version · cost the receipt and the thing The service deletes what it made seventy-two hours later. One Cottony run recorded the settings it proved and not the mesh, so the receipt outlived the thing that was bought.
  • The silhouette is checked before the credit moves. The request carries the drawing it has to match, and a mismatch is a refusal that costs nothing.
  • A ceiling agreed once, not a question each time. The rule that an agent may not spend on its own judgement is right; enforcing it by asking every time is what stops work between approvals. The budget and the ledger are files in the project.
  • What was bought is kept. The service deletes what it made seventy-two hours later. The mesh is copied locally with its sha256 and a provenance sidecar — prompt, seed, service, version, cost — so the receipt never outlives the thing.
  • The schema is learned once and written down. Finding out what a service accepts takes a series of deliberately invalid payloads, each with a made-up field as its control. That knowledge is recorded rather than rediscovered.
  • It arrives in this project's axes. A fetched mesh comes at an arbitrary orientation, scale and origin — a hammer standing upright where the drawing leans it. Normalising it is one conversion on the boundary, not two angles found by re-rendering until it looks right.
  • The background does not become geometry. A photograph of a plush toy returned the logo the toy was sitting on, fused into the mesh, and no camera move takes that back out.
One world

The engine's exit code is not the verdict.

Godot exits zero on a script error and non-zero on a clean quit. The only honest signal is a line the script printed and no error in the log — and every project that has driven it headless has written that parser again.

  • A capture runs where there is no screen. Headless mode draws nothing at all, which makes every screenshot a manual step on a developer's desk and keeps it out of any gate. capabilities() names the offscreen route that works on this machine.
  • Scale is derived, not tuned. One unit is one pixel today because somebody set a render rectangle to match a cell size the game holds separately, and nothing fails if either moves. Here the grid is declared once and both sides read it.
  • The runner is pinned. Language, locale and settings are declared, so the same script on two machines produces the same image — and a difference is a change somebody made rather than an environment nobody stated.
Motion

A pose needs a skeleton.

Every generative mesh arrives as a static surface, and rigging one by hand is the step that keeps character animation out of reach entirely. Everything below follows from fixing that one thing.

  • A clip, not a second static render. A settle expressed as a squashed re-render of the same mesh is right for one beat and has no way to say what a walk or a reaction would be.
  • Curves as text. A timing change inside a binary track is invisible in a diff and unreachable by an edit, which makes every adjustment a re-export from a tool nobody scripted.
  • One source, two outputs. The same settle is frames a 2D interface crossfades to and a clip a 3D scene plays. Authoring it twice and keeping the two in step by hand is the work being removed.
Geometry

A shape is data, not a program.

Cottony's tray, star, ball and props are four modules of imperative geometry code, and the shape each one describes is not readable without running it. A declaration is the same shape in a file a person can read, a search can reach and a reviewer can argue with.

tray.geom.toml [[part]] op = "extrude_outline" outline = "rounded_rect" radius = 0.06 height = 0.018 [[part]] op = "radial_array" count = 6 BUILT six seats, one radial array reachable by the search radius and height are numbers in a file, not constants inside a module nothing can reach reviewable before it is built two wrong constructions of one tray each looked reasonable while being written
  • The vocabulary is read off real assets. Extruded outlines, crowned plates, radial arrays, annuli and boolean pockets — because a format covering only primitives would leave every real asset back in code.
  • Its numbers are reachable. Geometry constants sit inside a module while the rig's sit in a dataclass, so a search can reach the lighting and never the shape it is lighting. In a declaration both are fields.
  • It can be reviewed before it is built. Two wrong constructions of one tray's seats each looked reasonable while being written and were only visible once rendered, which is the expensive place to find out.
  • And there is a way back to code. Any format will meet a shape it cannot state, and forcing that shape into the format produces worse geometry than the script it replaced. The escape hatch is part of the design, not an admission.
Fixed once

Six decisions nothing downstream has to guess.

Stated in docs/specs/tool-surface.md so that no other spec has to repeat them and no ingest has to invent one.

Axes
Y up, −Z forward, right-handed

the engine's convention, because the engine draws the result

Scale
one unit is one metre

unless the asset declares pixels_per_unit

Origin
centre of the footprint, on the ground

the base of the silhouette, not the middle of the box

Colour
sRGB, everywhere a caller can see

linear values live inside a renderer and never in an interface

Angles
degrees in authored documents

radians nowhere a caller can see

State
.polyweave/, inside the project

a cache in a home directory is state a colleague cannot reproduce

In depth

The seven blocks, one page each.

Each block on the roadmap is a group of lines that share a failure. These pages carry the lines themselves — the symptom in the roadmap's own words, and the measurement behind it.

A

What a call costs the turn

Handles for anything slow, a post-condition on every operation, typed errors that name the remedy, a surface that describes itself and this machine, and configuration resolved per call.

6 lines
B

Seeing the result cheaply

A surface is read on a sphere in three seconds, a render returns its measurements in the same answer, a distribution is reported as a set rather than a mean, and a sheet shows an asset beside its siblings.

5 lines
C

The asset compiler

What counts as a correct render, written as a file a search can aim at; a search that finds the rig numbers instead of a person finding them by hand; a cache so nothing renders twice; and a trace of everything the search rejected.

4 lines
D

Fetching without surprise

A silhouette gate before the credit moves, a ceiling agreed once instead of a question per fetch, the mesh kept because the service deletes it at seventy-two hours, and a fetched asset normalised into this project's axes.

6 lines
E

One world with the engine

A Godot run judged by what the script printed rather than by an exit code that means nothing, a capture that works where there is no screen, a scale derived instead of tuned, and a runner pinned so two machines agree.

4 lines
F

Motion

A fetched mesh rigged so it can be posed at all, motion as a clip rather than a second static render, curves as text a diff can show, and one source for both a 2D sprite and a 3D scene.

4 lines
G

Geometry as a declaration

A shape stated as data rather than written as a program: a vocabulary read off real assets, numbers a search can reach, a review before anything is built, and a way back to code for the shape no format can state.

5 lines
Deliberately not

What this will not be.

Five constraints bind the project, and they are governed alongside the backlog: a proposal is read against them before it becomes a line. The sharpest is the third.

A graphical editor

The caller here is an agent in a terminal, so a surface that needs a person at a screen to operate is one the agent cannot use at all.

Replacing Blender, Godot or the generative service

Those three already do the work; what is missing is the loop around them, so this orchestrates and measures and never re-implements a renderer, an engine or a mesh generator.

Spending money on the agent's own judgement

A fetch draws on a real balance, so the ceiling is a person's to set and the plugin's job is to make a spend bounded and visible, never to decide one is worth it.

One project's palette, rig or paths compiled in

Cottony is the first consumer and not the specification, so anything it needs that a second project would not is configuration, and a default that cannot be overridden is a defect.

A hosted service or an account to sign into

Everything runs on the developer's machine against files in their own repository, because a plugin that needs an account is one that fails on the day the account does.

The last block

Measured against what it replaced.

Cottony is the first consumer and the test of whether the configuration boundary holds — an adoption that needs a fork proves that it does not. It is also where the claim gets settled: nothing today records how long a correct asset took before and after, so there is no way to tell a real improvement from a rearrangement of the same work.

Until that line ships, every number on this page is Cottony's cost and not polyweave's saving. That is the honest state of it, and it is why the benchmark is on the roadmap rather than in the pitch.

📋PW35the plugin's value is asserted and never measured against what it replacedNothing records how long a correct asset took before or after, so there is no way to tell a real improvement from a rearrangement of the same work.
📋PW36a real project cannot adopt the plugin without carrying its own paths and palette into itCottony is the first consumer and the test of whether the configuration boundary holds, and an adoption that needs a fork proves that it does not.