Repository navigation
Synthesis: place three positions, compare every four-bar they admit, insert one - #243
Conversation
Three positions of a rigid body fix three positions of every point on it, and three points determine a circle -- so the centre of that circle is a ground pivot the point can be pinned to. Synthesis has always done that twice, for the two ends of the end-effector link, which is why it has always had exactly one answer. The coupler does not have to be pinned at the ends. Sliding the two pins along the link, or past it, moves both circle centres and gives a genuinely different machine through the same three positions. Enumerating those turns synthesis from "here is the answer" into "here are the answers, compare them". Each one is then asked the question the construction cannot answer for itself. It closes at all three positions by definition; whether it reaches all three on *one assembly* is a different matter, and a linkage that has to be taken apart between two of them is useless as a machine however exactly it fits. That is a branch defect, and it is the first thing a reader comparing candidates needs to be told. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Synthesis used to write a four-bar onto the grid on every nudge of a coordinate. That made the one thing this mode is for impossible: looking at a second solution destroyed the first, so there was never anything to compare. So the design and the answer are now two things. SynthesisBuilderService holds what was asked for -- three positions, the coupler they belong to, and what a solution has to satisfy -- and gains the state the redesign needs: which screen the reader is on, whether the next click drops a position and which way it is turned, and the ground-pivot region. SynthesisSolutionService holds what came back, and touches the grid exactly once, when Insert says so. Positions are placed rather than created at the origin, so removing one closes the gap it leaves: the panel fills three numbered rows in order, and a hole in the middle is a state the gesture cannot produce and cannot repair. The pose gizmo's old parts go with it. The X/Y arrows, the rotation circle and the status colours were the whole of SynthesisConstants and most of SynthesisPose; the handles that replace them are drawn from the design itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel used to be a form: type a length, create three poses, and a linkage appeared. It is now the account of a search. What is being synthesised, the coupler, the three positions as rows that read the same before and after they are filled, what a solution has to satisfy, an explicit Generate, the candidates side by side with what each one can and cannot do, and the one button that puts a solution in the drawing. Every requirement says what it is costing rather than only what it is: switched on it narrows the search, off it widens it, and when nothing is found the panel names the one to relax rather than leaving the reader to guess. The purely-geometric explanation is kept for when no requirement is in the way, because with one switched on it is the wrong answer. Its width goes to 400px, the analysis panel's. Three numbers per position, a gallery read side by side and a transport do not fit in 250, and all three are comparisons that stop working the moment they wrap. The stylesheet ships through the theme mixin nested under the panel's own id: it is twice the component-style budget, and half its class names are words rather than names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Placing is opt-in: Add position arms the canvas, the ghost follows the pointer, the wheel turns it, and a click drops it. Synthesis shares the canvas with the drawing, and a mode where every click makes something is a mode where every click meant as "look at this" makes something instead. A placed position wears the dashed box, corner grips and turn knob the tracing underlay wears, because it is the same kind of gesture -- something being placed on the grid rather than built. Its corners pull the one dimension a position has: how long the end-effector is. The chosen candidate is previewed live, tinting each position by what it does with it, and hovering another keeps the picked one on screen faded behind it -- without that, moving along the gallery replaces the linkage with no way to see what it replaced, which is the one comparison the gallery exists to make. Three things had to give way for the gestures to work. The pan guard used to recognise synthesis by what was last clicked, which never stopped being a pose, so the canvas could not be panned again until something else was selected; it now asks whether a gesture is actually in flight. The wheel is handed over through svg-pan-zoom's own API while a position is waiting to be dropped, because swallowing the event depends on which listener was registered first. And it is handed back from one place, since a missed pairing leaves the canvas with a dead wheel until the tab is left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Undo and redo are a stack of URL strings. A design that is not written into them cannot be stepped back through -- and now that synthesis holds work of its own for as long as it takes to compare seven linkages, that work is exactly the kind a reader expects Undo to reach. A shared link mid-design opened on an empty panel for the same reason. It rides in the trailing section the lock marks opened, tagged 'S', which no lock or centre-of-mass anchor uses. Written only when there is a design to write, so a document with none encodes to the bytes it did before any of this existed -- the same bargain that section was built on, and the existing URL specs still compare the same strings. Unlike a lock or an anchor it names nothing the URL carries, so there is no reference to resolve; what it can be checked against is its own shape. It fails closed on an entry that is short a number or carries a tag nobody wrote, because a design half-read is worse than none: the panel would come up with positions that are not where the reader left them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… six-bar The suite it replaces checked that synthesis added a mechanism rather than replacing the drawing -- a promise about a build that no longer happens on its own. What is worth checking now is mostly the machinery around the new promise: that the canvas gestures do not fight svg-pan-zoom, that nothing reaches the drawing before Insert, that the preview stops being drawn once the real thing is there, and that the design survives undo and redo. Selectors are scoped to the panel's own id. Half these class names are words rather than names -- card, row, note -- and the app has its own elements wearing them: an unscoped `.card` matched six elements and quietly clicked the wrong one, which read as two features being broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing else is The status strip reports on the drawing, and in Synthesis the drawing is not what the reader is working on: a design in progress is not on it at all, so "Nothing to analyse yet" was true and useless for the whole of the work. It now says where in the search they are -- which position is about to be dropped, that three are placed and ready, which solution is being looked at and what it does with the positions -- and after Insert it says what was left behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for pmksprod ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The one that mattered: a position was dropped only on a press released within a tenth of a second. `pastDragThreshold` calls a slow press a drag, which is right for a part already on the grid -- holding still is how you take hold of something -- and wrong for aiming at an empty spot, where a deliberate click is the normal gesture. Every slower click was thrown away, which is why placing a position seemed to need several. Distance is the only thing that tells the two apart here. The rest: - Generate reports itself for at least a second. The search is real work but on a small design it finishes inside one frame, and a button that answers that fast reads as though nothing happened. The floor is under the progress state, not the work: a slower search simply takes longer. - No position row is selected until one is asked for. A highlighted row before anything is placed reads as "your first position went here", which is exactly what it is not. - The chips on the grid get the pill the design gives them. They are read against whatever the drawing puts behind them, and grey words over a link are words nobody can read. - "2 of 3 required" named an obligation the reader does not have -- it looked like two had to be switched on before anything would happen. It says what the number reports instead: how much is being asked of a solution. - The three values on a position row share the space rather than each holding a fixed width. Fixed, every one had to be sized for the longest value it might ever hold, and they could not all be -- so whichever lost had its unit cut off. That was "301 deg" arriving as "301 de". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s beside it
Two things were missing, and they turn out to be one thing.
Insert used to be a one-way door: it added a machine, switched itself off, and
forgot which machine it had added. So the loop the mode exists for -- try a
solution, look at it, change a position, try the next -- was not available. And
the positions vanished the moment the reader left Synthesis, taking with them
the only record of what the linkage on the grid was *for*.
Now the design owns the joints it inserted, by id, and the ownership rides in
the URL with the rest of it. Inserting again revises that machine in place.
Nothing else in the drawing is ever touched, which is what makes this safe in a
project that already holds work: another machine can sit beside it and Insert
will not reach it.
Ownership answers one of four things, and each calls for something different:
none nothing of ours is there -- first insert, or Undo stepped back
past it, or the reader deleted it. Insert simply inserts.
ours exactly what we wrote. Insert replaces it without asking; it is
our own previous answer.
edited still ours, still separable, but moved by hand. Insert stops and
says what would be lost, offering the two things the reader could
mean: replace it, or keep it and insert a new one beside it.
entangled pinned to another machine, or half deleted. We can no longer take
it back cleanly, so we stop claiming it.
A warning on entering Edit was the other candidate for this, and the
multi-mechanism case is what ruled it out. With a second machine in the
drawing, going to Edit to work on *that* is an ordinary, innocent act -- a
warning that fires on it is both wrong and quickly trained away, and one that
severed the link would cost the reader their synthesis loop for doing something
unrelated. Warning at the moment the work would actually be lost is later but
exact, and it can say what is at stake rather than what might one day be.
Which leaves nothing needing to be said at the boundary, because the positions
say it themselves: they stay on the grid in every mode, faint and out of the
way of every click, with the canvas menu to clear them -- one at a time from
Synthesis, or all of them from anywhere. Clearing them is not an edit to the
mechanism, so unlike everything else on that menu it does not wait for the
start pose.
Two smaller things fell out of it: a right-click while placing was armed
dropped a position, and the analysis modes cleared the whole context menu
rather than only the half of it that edits the drawing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Feedback round. Six direct fixes in The click bugPlacing a position went through The rest of the small ones
The design questionThe proposal was: free movement between Synthesis and Analyze, and a warning on entering Edit that manual edits would sever the link. The trigger is what I changed, and the multi-mechanism case is why. With a second machine already in the drawing, going to Edit to work on that is an ordinary, innocent act. A warning that fires on it is both wrong and quickly trained away — and one that actually severed the link would cost the reader their synthesis loop for doing something unrelated to synthesis. So the link breaks when the synthesised linkage is actually changed, not when a mode is entered. Ownership is per-machine, by joint id, and rides in the URL — so it survives a reload and steps back correctly when Undo removes the insert. Ownership answers one of four things:
Which leaves nothing needing to be said at the mode boundary — because the positions say it themselves. They now stay on the grid in every mode, faint and Two bugs fell out of building it: a right-click while placing was armed dropped a position, and the analysis modes cleared the whole context menu rather than only the half that edits the drawing. Verification
🤖 Generated with Claude Code |
…back to Generate Eleven notes from a read-through. Three of them turned out to be the same thing, and one was a rule that made the mode tedious to use. **The preview looked unlike the drawing because it was drawn by different code.** Positions, previewed solutions and ghosts were strokes on a line; the drawing builds a filled outline at a quarter of the object scale, paints it at 0.7 opacity and strokes it in its own colour. The widths matched by arithmetic and nothing else did. They now share one capsule builder, and the preview's colours come from the same ColorService `insert` asks -- so it cannot promise one thing and the drawing deliver another. **Everything was small because the old panel kept the object scale in step and the rewrite did not.** It called `updateObjectScale` whenever a pose was created. Restored for the first position on an *empty* drawing: object scale is a global, and resizing every bar in a drawing that already holds work because a position was placed would be a change nobody asked for. **"Driven from" moved nothing a reader could see.** Both ground pins were drawn identically, so the control looked broken. The driven pin now wears the motor's mark, from the same two assets the drawing uses -- which also makes visible the thing that is otherwise hard to explain: with a driver fitted, neither ground pin is the input at all. **A nudge is a different answer, not a different question.** Moving a position sent the reader back to Generate, which made the button the thing they spent the session pressing. The search now keeps up with a move on its own -- it is keyed on the design, and the chosen candidate is identified by where its pins sit on the link, so it survives. Only a change to what is being *searched for* -- a position added or removed, a requirement switched -- goes back to Generate. The search holds still through a drag and catches up on release: the positions follow the pointer either way, and re-running a full crank revolution per candidate on every pointermove would stutter for an answer nobody can read mid-gesture. **Four corner grips promised two axes of scaling and delivered one.** A position has a place, a heading and a length, so it now has one handle each: the body, a knob above it, and a square grip off the front end on the bar's own axis, which is the direction it actually pulls. The rest: the design's sections fold away like every other panel's; a lone candidate is not given a gallery of one to be compared against, nor a letter to go looking past; the three solution rows had two different label sizes because `font: inherit` on a button reaches a row that sets none; the transport's direction arrow was clipped by a 22px button holding a 24px icon box; the requirement count is gone; and the marks on the track lost their white ring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…thing Insert is the one moment in this mode that changes anything, so what has not happened yet must not look like what has. The previewed linkage is now drawn with a broken outline and a lighter fill: still in the colours it will be built in, so the reader can see what they are choosing, but unmistakably an offer rather than a part. The gap under the Positions heading was an empty header row. It carries the Add position button, and with all three placed there is no button left to put in it -- so it stood there as a band of air between the heading and the first row. It is drawn only when it has something in it. And a heading over a gallery says what there is to choose between; over a single solution "1 linkage works on one assembly" only repeats, at greater length, what the section under it already says. One thing found on the way: decoding a URL replaced the design without telling the search, so candidates found for the last design could outlive it. A decode is as structural as a position being removed, and now says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The copy scan found the confusion behind the yellow box. The first section was titled "Coupler" and held a Length field belonging to a different bar: the coupler is the four-bar's own floating member, pinned *to* the end-effector link, and only the same length when it happens to be pinned at its ends. That is exactly what the first requirement decides -- so "letting the pins slide" and "Coupler is exactly 5.00 cm" were two readings of one word, and the note that used both read like a contradiction. One word per concept now, everywhere including the status strip and the grid: position never "pose" end-effector link the bar being positioned -- and the section's name coupler only ever the four-bar's own member solution one of the four-bars found; never "candidate" linkage the machine itself ground pin never "ground pivot" requirements never "criteria" The requirement is named for the choice rather than its consequence -- "Coupler pinned at the link's ends" -- and the note that explains what to relax now says so in those words, always offers moving a position as the way out that costs no requirement, and sits *below* the requirements rather than among them: it is not one of them, it is what they have jointly done. Space, in a panel that has little to spare: the Positions buttons move into that section's own heading; the strictest requirement is offered first; the gallery keeps three columns when it is opened out rather than re-flowing to two and making the reader find their place again; a lone solution loses the letter it had nothing to be told apart from; and the results section is drawn only when it has something in it, which was the band of air above "Solution". A driver that cannot be fitted now turns its switch off and says why on hover. It used to be a paragraph appearing under a switch that had just been flipped, explaining that what was asked for had not happened. Clicking empty grid lets go of the selected position, as it does everywhere else in the app. And the footer's Delete says what it deletes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report was that a solution claiming all three reached only the first, and it was two faults wearing one symptom. Both were found by driving the linkage rather than asking it, which is what the new spec does over four hundred random designs. **Reached meant solvable, not driveable to.** `assess` worked out which positions the loop closes at, and separately how far the crank can actually turn; it never compared them. The loop can perfectly well close at an angle the crank cannot reach -- the circles intersect again on a stretch of the curve reachable only by taking the linkage apart, which is precisely what a branch defect is. So a position now counts when the loop closes there *and* its crank angle lies inside one continuous run of travel from where the linkage is drawn. **And a linkage can reach a position and stall on it.** At a dead point the transmission angle goes to zero, the coupler pin moves hundreds of units per degree of crank, and no force turns it. On paper it passes through; in metal it arrives and stops -- which is what "gets stuck" looks like. The number was already on the card: the solution in the report read "min angle 11°". Solutions whose worst transmission angle falls under fifteen degrees are no longer offered as reaching all three, and where they are shown at all the card says "stalls at 11°" rather than dressing it as a minimum. **The six-bar flickered because the preview was driving the wrong linkage.** The dyad is sized to carry the input across the span the three positions occupy, not across a whole revolution, so past that span its crank and coupler no longer reach the pin they drive and the elbow has no solution. The preview, solving per frame, dropped the driver's two links for those frames. With a driver fitted the transport now offers only the travel the six-bar can actually make -- which is also the honest thing to offer. Three smaller ones. The region's four numbers are plain, with the unit said once for all of them, because four values each carrying "cm" cannot also show their digits. Every position now draws the point its coordinates are measured from, so Back, Center and Front move something visible rather than only making the numbers jump. And a position is drawn round at the back and tapered to a point at the front: a capsule is unchanged by being turned half a revolution, so a design that solves perfectly once a position is flipped was impossible to see -- the drawing of the wrong one and the right one were the same picture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The six-bar barely moved, and on the design in the report it moved four degrees. The preview was running the train backwards: it stepped the four-bar's own crank and drew a driver onto whatever came out. But once a driver is fitted, the four-bar's crank is not an input at all -- it is an output, rocking back and forth as the driver goes round. Stepping it therefore covered half a stroke, once, and then ran out of angles at which the dyad could close at all, which is where the four degrees came from and why the links flickered out. Now it is solved forwards, the way the inserted mechanism is solved and the reason that one always ran correctly: the driver crank turns, which places the elbow, which places the pin it drives, which sets the four-bar. On the reported design the preview goes from four degrees of travel to a full revolution, and its five links hold together across every sample of it. Open and Crossed are one solution with a switch on it. They are the same four bars, the same pins in the same places, closed two different ways -- so listing them as two entries asked the reader to compare a thing with itself, and used up half the gallery doing it. One card per construction now, and the letter belongs to the construction, so flipping the switch does not rename the solution under the reader's hand. And the position shape goes back to the capsule every other link on this canvas wears. Shaping the outline did tell the two ends apart, but it cost more than it was worth -- the bars stopped looking like the links they are. Which way round a position is is now said inside it, with a chevron three quarters of the way along pointing at the front, which is how a drawing normally says it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s not one Generate and Insert were never both available: one of them is always the step the reader takes next, and the other is not a choice. Having them as two buttons in two places meant hunting for whichever was live -- and the Generate one lived in the scroll area, so it could be scrolled off the screen at the moment it mattered. They are one button at the foot of the panel now, which reads Generate solutions until there are solutions and Insert into grid after, with the search reporting itself directly above it. It names the search even before there is anything to search, greyed. Reading "Replace on grid" at two positions placed is true and useless: what is actually next is the search, and the reader is one position from it. And a driver that will not fit these positions greys its switch, rather than only explaining itself to whoever thinks to hover. The reason is still there on hover; this is the part that says not to bother pressing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r full width Three from a read-through. The sentence under the progress bar described the search accurately and was gone before anybody finished reading it, which is a sentence doing no work. The bar says the same thing and says it in the time available. A finished search puts its answer at the bottom of a panel the reader is looking at the top of. Nothing above it changes, and the button they pressed is in the foot, which does not move -- so there was no cue that anything had happened down there. The panel now scrolls to meet it, after the frame that renders it, and without the animation where the reader has asked for less of that. And a section header is a target that runs edge to edge. The inset sat on the header row, so the toggle's own background -- its hover, and its hit area -- stopped short of both sides, leaving a strip beside every heading that looked like part of it and did nothing. The inset belongs to the text, so it moved to the button's own padding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ferent one The flat side on that otherwise curved path was a line ruled across a gap. The trace asked the *four-bar* where it would be at each step, while the range it was stepping belonged to the *driver's* crank. Two different cranks, so the angles meant nothing to the solver they were handed to: with a driver fitted, eighteen of sixty samples did not close at all, and the loop skipped each one and carried on with a line -- drawing a straight edge across a region the linkage never visits. It now steps the same solve the preview itself runs, and where a phase genuinely will not close the pen lifts rather than ruling across. Sampled at 240 rather than 60, so a stretch the coupler crosses quickly is drawn as the path it takes rather than one long chord over it. Checked against the real thing rather than by eye: the previewed path and the frames the solver produces for the same linkage, once inserted, agree to within eleven internal units -- four hundredths of a centimetre -- in both directions. The dividers were two rules leaning together. Each section carried a bottom border and each section's header a top one, so every boundary in this panel came out at twice the weight of the ones everywhere else. The header draws it; the section no longer does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…not fail An outside review found four real bugs and one test of mine that was proving nothing. Taking them in the order they matter. **The transmission angle was sampled, and the sampling missed the drops.** It walked the stroke every two degrees and rounded, so a candidate reported at sixteen degrees measured four and a half between two samples -- and was offered as reaching all three positions. It does not need sampling: the angle follows from the distance between the crank pin and the far ground pin by the cosine rule, and that distance is extreme only at the ends of the stroke or where the crank points at or away from the far pin. Four angles to check, exactly, and compared against the cutoff unrounded. Fixing that exposed the real fault underneath. The stroke was built from raw atan2 angles measured against a walked range that can sit a whole turn away from them, so the interval was often offset -- and sometimes inverted, and therefore empty, in which case the answer came from two arbitrary endpoints. The same class of bug as the reachability one, in the same function. **And my test for it could not have failed.** It asked whether any defect-free candidate had a transmission angle under the cutoff, which `defectFree` is defined to make impossible. It now measures the angle independently, from the bar lengths, and compares -- across three hundred designs, and stepped so both ends of the interval are always sampled, because the angle collapses at a travel limit and that is exactly the sample a rounding error drops. **A driver was offered that could not turn all the way round.** `driverDyadFor` sizes a crank and coupler to carry the input across the arc the positions need and stops there; it never asks whether the four-bar closes everywhere in between, and sometimes it does not. The panel promises one full turn, so availability now requires one: the same forward walk that feeds the transport decides whether the switch may be pressed at all, so the two cannot disagree. **A corrupt number in a URL was absorbed rather than refused.** The validator counted fields but never checked that a number was made of characters the encoder emits -- and an unknown character decodes as -1 rather than failing. A single bad digit in the coupler length brought back all three positions around a link that was not the one shared. That is the half-load the validator exists to prevent. It now checks the alphabet, and refuses a design that describes itself twice. **A drag released off the canvas never ended.** The canvas hears pointerup only on itself, so a release over the panel left the gesture live: the canvas could not be panned and the search stayed frozen until something else was clicked. The pointer is captured for the duration, with a window-level release behind it for the pointers that cannot be captured. Gestures are also now left-button only -- a right press is asking for the menu, and taking the pointer took the menu with it. Two smaller ones. The panel said pins could be pinned "anywhere ... at any length" and offered to "search for any four-bar", while the search tries nine placements and shows at most eight constructions; it now says what it does, and a heading that counts more solutions than can be opened says how many are shown. And the rewritten rows had lost their keyboard and their names: a position row is a button again, the delete is a button, and every field carries its own label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Driven from Pin A". There is no Pin A on the screen. Nor a B, C or D, so "Ground A–D" and "Coupler B–C" in the link lengths named bars the reader had no way to identify either. The preview draws its pins' letters now, whatever the show-ids setting says, because the panel is talking about them either way. Looking for more of the same turned up three others. The link lengths were half one thing and half another: two rows named their pins and two did not. All four do now -- crank A–B, coupler B–C, rocker C–D, ground A–D -- and the driver's two say which pins they run between as well. "Coupler pinned: at both ends cm". The unit was appended to a phrase rather than sitting beside a number, so where there was no number it read as that, and where there was one it read "3.0 past the back cm". The unit goes with its number. The transport said "full crank rotation" beside a six-bar, where the crank being turned is the driver's and the four-bar's is an output. It names which. And Space did not press a focused button -- anywhere in the app, not just here. The shortcut service skips a keystroke aimed at a text field, but a button is not a text field, so Space reached the global handler and was preventDefault-ed on its way to the button it was aimed at. Every button in the app was reachable by keyboard, focusable, outlined, and inert when pressed; nothing noticed for as long as nobody tried to drive it without a mouse. Space and Enter now belong to whatever natively answers them, and every other shortcut still works with a button focused. Also from the second review: the stroke a solution is judged over is the shortest arc between its three positions, found by looking for the widest gap between them. Taken as the smallest and largest of the three angles it named the long way round the circle on a full-turn crank -- three hundred and thirty degrees where the real arc was eighty -- so linkages were being judged on travel they never make between the positions, which rejected good candidates and accepted binding ones. The test for it checks the stroke that was chosen, not only the angle computed over it, and fails on the old behaviour. The position row is a plain container again: it had been given role="button" so it could be reached by keyboard, which made it a button wrapping two textboxes and another button, and its Space handler swallowed the nested button's own activation. The numbered chip is the control instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ind home Three from using it. The ghost was drawn small and the position it turned into was drawn large, so the click appeared to grow it. Object scale decides how big parts are drawn and it was being fitted on the first click -- after the ghost had already been drawn at the old one. It is fitted when placing is armed instead, which is the moment before the ghost first appears. Still only on a drawing with nothing in it: the scale is global, and resizing someone's work because a position is about to be placed is a change nobody asked for. Driving from Pin D crawled. Reading a solution from its far pin re-assesses the swapped linkage, which walks a whole revolution a degree at a time -- seven hundred solves -- and that was done on every call. Drawing the coupler's path asks for it once per sample, two hundred and forty times, on every animation frame: something like a hundred and seventy thousand solves a frame. Pin A never noticed because that path hands the candidate back untouched. It is remembered now, and the two pins draw at the same speed. And committing from halfway round the cycle replaced the linkage on screen with a differently-posed one between two frames. What gets built is always the start pose, so the preview winds back to it first, over the same 220ms the app eases everything else home at -- and not at all where the reader has asked for less animation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three from the third review, all of them mine and all from the commit before this one. Insert was re-entrant. Each press started its own wind-back, so a double-press committed twice -- rebuilding the linkage and writing two history entries for one intention -- and a press followed by leaving the tab committed afterwards, onto a drawing the reader had moved on from. One wind-back at a time now, its own frame handle rather than sharing the playback loop's, and both cancelled when the panel goes. Fitting the object scale was put in the button's handler, which missed the other way in: clicking an empty position row also arms placing, and that route drew its ghost at the old scale. Both go through one method. And the wind-back interpolated raw angles, so near the end of a full turn it took the three-hundred-and-fifty-nine-degree route to somewhere one degree away. It takes the shorter direction, as the app's own easeToStart does. Measured: three rapid presses commit once; a press then a tab change commits not at all; both ways of arming fit the same scale; and winding home from 355 degrees past travels four degrees rather than 355. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fourth review found that a single Insert wrote two. Inserting rebuilds the mechanism through `updateMechanism(true)`, and that `true` is a save -- then the panel recorded again on the way out. One Undo stepped back over the second of the two and left the linkage sitting on the grid, which is the one thing Undo must not do. My own test could not have seen it. It counted calls to `solution.insert` and then took the linkage away by calling `undoInsert` directly, so it never went near the history it was supposed to be about. It presses Undo now, and requires the grid to be empty afterwards. Two more from the same review. The replace warning waits to be answered, by design -- but its answers act on this panel, and it was still on screen, and still clickable, after the reader had left for Edit. It goes when the panel goes, and its actions check anyway. And the wind-back's normalisation was [-180, 180] where the comment said (-180, 180]: at exactly half a turn both ways round are the same length, so it now goes forwards, which is the way the crank was already turning. The arming check I added last time pressed the button and then asserted about the row -- proving the route that worked and not the one that did not. It clicks the row, before the suite arms from the button, and hands back the state it borrowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five findings from the fifth review, and the falsification of my own answer to one of them. I had checked the scale-fitting guard myself and reported it sound: it fits only when `links.length === 0`. A joint on its own belongs to no link, so a drawing holding nothing but loose joints read as empty and arming resized them under the reader -- 140 to 360.9, measured. Emptiness now means joints too. Choosing "Driven from Pin D" renamed the linkage. Reading it from the far pin puts pin D in the field called A, and everything that drew a letter or named a bar took the field's name -- so the motor appeared beside a pin marked A, the crank became A-B, and the one control whose entire job is to say which pin drives was the control that made the letters stop meaning anything. Letters now follow the pins. Insert defers 220ms to wind the preview home and worked out what to build only on arrival, so choosing another card inside that window built that one instead, from a press aimed at the card before it. The press now remembers what it was aimed at and cancels if that changes -- a change of mind, not a redirection. After inserting, the preview was suppressed for any owned linkage rather than for the one being looked at, so choosing a second solution left the first standing solid while the panel offered to replace it: the replacement went in having never been shown. And the far-pin cache held one entry, which hovering a card and drawing the picked one evicted in turn -- the seven hundred solves it exists to avoid, just less often. Four tests asserted less than their names. The gallery's "three columns" never opened the gallery and passed on a flex row having no columns at all. The Space check dispatched a synthetic event and asked only whether anything prevented it, which proves nothing about the native activation that was being suppressed; it presses a real key on a real button now. `defectFree` was stated as `onBranchCount === 3`, half the rule, and true of that corpus only because nothing in it binds -- there is a binding corpus now, and dropping `!binds` fails it. And three of the six named designs offer nothing to walk, which the per-design walk passed on silently; the corpus is tallied instead. Each of the four fixes was reverted in turn and the check that covers it fails. The first attempt at that found nothing, because deleting a guard left a local unused, the compile failed, and the dev server went on serving the previous bundle -- a green run against code that was never loaded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Naming the preview honestly was half a fix. Insert assigned letters in field order, and reading a candidate from the far pin puts pin D in the field called A -- so the pin the panel had just labelled D arrived on the grid called a, under a dimensions list reading "Crank D-C" over a crank whose ends were marked a and b. The mismatch had moved rather than gone. The letters now follow the pins through insert as well, and links are named by their ends in alphabetical order rather than by relying on the letters having been handed out in that order to begin with. Reverting it fails the new check with the whole story in the detail: the motor drawn at (-394, -3997) and the joint built there called A. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anges Four from the sixth review. A design that put four joints on the grid and has since lost one has been cut into, and Insert leaves that alone rather than replacing it. The decoder dropped the ids of joints that were no longer there -- rightly, since a claim on a missing joint would be inherited by whatever new joint next took that letter -- but dropping them silently threw away the only evidence that anything had been lost. A shortened list looks exactly like a smaller linkage, so a reload turned "you cut into this, I will work around it" into "this is mine, I will delete it", and the reader's moved joint went without the warning that exists for precisely that. The fact now rides in the URL on its own, in a flag slot that was already being written as false, so nothing previously shared decodes differently. `needsReinsert` compared the four-bar's four pins and nothing else, so fitting a driver to an inserted four-bar -- or taking one off an inserted six-bar -- left the panel saying "Inserted into grid" over a drawing that no longer held what was being looked at, with the preview hidden because it agreed. It counts the joints and compares the driver's two as well. Leaving Synthesis for Edit saved the mechanism a second time, identically, because the old mode built onto the grid as the reader typed and something had to write that down. Nothing has set the flag that was supposed to gate it for as long as the redesign has existed; the redesign only touches the drawing through Insert, Undo-insert and Delete, each of which saves for itself. The first Undo after inserting therefore appeared to do nothing. Both the save and the dead flag are gone. And the letters. Yesterday's fix made the preview name the pins truthfully but still named them A-D, while insert took the next letters free -- so beside a single loose joint the preview promised D/C/B/A over pins that arrived as E/D/C/B. Preview and insert now ask one function for them, which is also what the "Driven from" control names its two ends after. Three more tests that asserted less than their names: the unit check passed if the units vanished entirely, the off-canvas drag check passed when there was nothing to drag and never established that a drag had begun, and the corpus tally required only one of the three designs that have answers to have one. Each of the four fixes reverted in turn fails its own check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
If every joint the design wrote has since been taken away it owns nothing, and there is nothing left to be entangled with -- but the flag was set on any shortfall, including that one, and a design carrying it writes a section into every URL it produces from then on. `ownership` already answered "none" in that case, so the flag was saying something no one asked and no one could act on. Verified separately that both of the reported designs still re-encode to the exact bytes they were opened with, so the flag slot has not shifted anything beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two from the seventh review, both of them my own fixes landing short. `previewLetters` counted every joint on the grid, including the linkage insert was about to take away. So a replacement promised E through J and then built A through F, and the dimensions list renamed itself off the linkage the instant that linkage was inserted -- "Crank E-F" over a crank standing on the grid as A-B. It now counts the grid as insert will find it: a replaceable linkage of ours releases its ids, and survivors of one that has been cut into do not, because those stay. `determineNextLetter` grew the parameter for it, and the six ids are worked out once per change of the grid rather than three times a frame. And being cut into now sticks from the moment it is noticed, not only across a reload. Ids are handed out again after a deletion: delete one of ours, draw a joint, and that joint takes the letter we just lost -- so the count came back up, every id was present, and the linkage read as wholly ours with somebody else's joint standing in it, to be removed without warning by the next replace. Reverting the latch drops it to `edited`, which warns and then deletes anyway. Four more tests. The greyed-driver check compared two things that were both false -- these designs raise no refusal, so it established nothing; it now sweeps every candidate and both drive ends and requires having seen the switch live, with the refusal itself covered where it can be built to order in driver-dyad.spec.ts. The full-turn check ignored a driver that was offered and never sized. And two `every()` calls were reported as agreement over empty lists. The replacement case is what let this through at 101/101, so it is a check now: reverting either fix fails the check written for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No product change: the eighth review found the code sound and these three tests unable to fail. The greyed-driver check had never met a design that refuses a driver, so it compared two false values and would have passed with the binding deleted. Refusals turn out to be rare but reachable — three positions needing more than half a turn at the input — so there is a design here that produces one, and the switch is now read on both sides of it. Finding that design cost two false alarms worth recording. The check first "failed" because it read the DOM in the same turn as the state change, before Angular had drawn it. Then the falsification "passed" because the template holds two `[disabled]="!!driverRefusal"` bindings — the text button and the switch — and I removed the first one, leaving the switch bound as it always was. The dashed-preview check asked whether every rendered bar was dashed, which an empty canvas answers yes to; it requires the bars to be there. And the reused-id check stopped at "still entangled" without ever performing the replace it was about. Adding the replace was not enough either: it looked for the reader's joint by name, and removing that joint frees its letter for the very next insert to hand straight back out, so a joint called D exists either way — somewhere else, belonging to somebody else. It is found by where it is now. Reverting each of the three fails the check written for it: the class off the bars, the binding off the switch, and insert taking an entangled linkage away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ninth review found the defect the last four passes had been circling. Where insert put each joint was held in memory and nowhere else. Open a shared link, or a state restored by Undo, and that record was empty -- and an empty record meant "nothing has been touched", which is the one condition under which Replace helps itself. So: insert a linkage, drag a joint somewhere you want it, share the link, open it, press Replace, and the joint goes back where synthesis had wanted it, with no warning, because the warning is for linkages that have been edited and this one had forgotten that it was. The same blindness made a deleted-then-reused id read as ours to delete, and made Undo after a replacement report the linkage it had just restored as edited, because the record still described the linkage that had replaced it. The baseline is written down now, in an entry of its own beside the ids. Its own entry rather than beside them, because an id is a letter and a number is base-N over an alphabet that includes letters, so one entry holding both cannot be read back without guessing. And the default when there is no baseline is now to ask rather than to assume. Every insert writes one, so the only way to be without it is a URL from before this existed -- and asking costs a click, where assuming cost the reader their work. Four tests that did not establish their names. The duplicate-entry test doubled an entry without fixing the length, so the checksum rejected it first and the test passed on an error that would still be raised with the duplicate rule deleted. The naming test asked whether the first candidate was called A, which every candidate being called A satisfies. The Open/Crossed check proved the two assemblies were not two cards but never touched the switch that replaced the second one. And the greyed-driver sweep is renamed to what it proves: these designs never refuse a driver, so it establishes agreement, not greying. The reader's linkage untouched, and the reader's linkage moved, are both checked across a real shared link -- both are needed, because without a baseline the safe answer to "was this moved" is "ask", which an unmoved linkage would get too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One conflict, on the line two branches happened to add an import to. The base had already taken the intro.js tour out, so the import this branch still carried had nothing left to use it; the incoming `SvgArrowComponent` is in the component's own imports list and stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…omises Seven findings from a GPT-5.6 review, four of them correctness. The force-graph row counted `joint.links` plus ground and called that "how many parts meet here". A floating slider's carrier is deliberately not in `links` -- the slider rides the bar rather than being one of its members -- so every Whitworth, Scotch yoke and shaper quick-return was told "one part meets it" while the force solver was generating a reaction against two. This is exactly the wrong-reason failure the redesign exists to stop, and it was mine: I wrote a count instead of asking the model, and said so in the comment at the time. It asks `jointHasForceToGraph` now, which reads the reaction index the panel reads. Two deletion rows promised less than the deletion does. On a welded compound `linksRemovedByDeleting` asked only the compound's own joint count, so a four-joint compound looked safe while the two-joint leaf inside it was doomed: it is asked leaf by leaf now, which is what the deletion actually does to it. A cylinder joint hard-coded "Delete Joint and Cylinder" and swallowed any neighbouring bar the mount was also holding; that bar is named. "Locked means undeletable" was a claim the menu made and nothing enforced. The Delete key and the panel button reach the same joint, and both deleted locked parts happily -- so a greyed row sat beside a live keystroke doing the opposite. The rule is `deleteRefusal` on the service now, and the menu quotes it. Clearing the whole drawing is the one caller that passes it by, because that is a different act from deleting a part. And the card was a snapshot with live keys behind it: pressing K with a joint's menu open locked the joint and left the menu showing the unlocked state, Delete still enabled, and clicking it deleted the joint that had just been locked. Any shortcut closes the card now. Duplicate Link copied a shape rather than a body -- a bar carrying seven grams and a hand-set moment of inertia came back with neither, and a force analysis that no longer agreed with the original. Mass, inertia, fill, disc and the CoM anchor come with it; the name and the lock do not. The review also found the e2e suite had not been swept: eleven suites still drove `cMenuItems` or `#contextMenu #menu-item` and threw on the first menu they opened, which reads as a broken app rather than a stale test. They are migrated, and two of them caught things worth catching. `phase4-stack-and-menu` holds §4.1's rule that a slider is *offered* the weld and refused with the reason -- I had hidden that row on the design's say-so, and the model does not agree, so it is back. And the sweep found the builder throwing outright on a SliderBlock handed to it as a link: not reachable by pointer, but a builder that throws on a shape it was given is worse than one that says little. Left alone: the synthesis section of `circular-link-and-driver` still fails, because #243 renamed the panel's model and reshaped the flow it drives. The field rename is fixed here; the rest is that suite's own stale test of a feature that changed shape, and it fails the same way on the base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the
Synthesis Prototype.dc.htmldesign from the Claude Design project.Retargeted to
multi-mechanism-redesignnow that #241 has merged.What changes
Synthesis used to build one four-bar onto the grid and rebuild it on every nudge of a coordinate. That made the one thing the mode is for impossible: looking at a second solution destroyed the first. It is now a search.
Where the extra solutions come from. Three positions of a rigid body fix three positions of any point on it, and three points determine a circle — so that circle's centre is a ground pin it can be pinned to. Doing that for the link's two ends is the one construction synthesis has always done. The coupler does not have to be pinned at the ends: sliding the pins along the link moves both circle centres and gives genuinely different machines through the same three positions.
Bugs found and fixed along the way
pastDragThresholdcalls a slow press a drag, which is right for a part on the grid and wrong for aiming at an empty spot.Verification
synthesis-driveable.spec.ts, which drives 400 random designs rather than asking them. Falsified: reverting the reachability fix makes it fail on a named design.e2e/synthesis-redesign.mjs.🤖 Generated with Claude Code