Workshop: two windows — Model Splitter and Model Fuser - #63
Conversation
…Model Fuser — on one implementation User: "the Model Workshop has too many responsibilities, which makes it cluttered — split it up into two screens, one for cutting and one for merging." ModelWorkshopWindow is abstract with one flag, Fusing; the Cutter and the Fuser derive from it and share the file pickers, the probe, the filtered part list, the preview, the mirror finder and the keyboard sweep. The Cutter shows the Split checkboxes, "Check all splittable", "Hide already-whole", the plane cut and Split (output _split / _cut); the Fuser the ⊕ letters, the A–Z keys, the weld slider, Save/Load groups and Fuse (output _fused). "Show only" lists each window's own kinds. The one-step Generate (fuse AND split) went with the split: chain the outputs — the ⊕ letters travel with a cut or split output, so either order works. Menu: Tools ▸ HAF ▸ Model Cutter / Model Fuser; docs and the quickstart follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, every hint speaks the window's own language Found reviewing the two-window split against the running code: (1) the Generate(alsoSplit) split branch was unreachable once the button went — FuseMarked is the whole method now; (2) the mirror finder told the Cutter to "press a letter" (Space checks there, and a whole part cannot be checked at all); (3) the namesake warning, the Output GLB tooltip and the no-preview note all said Split in the Fuser; (4) the Fuser's rows were dimmed as "already whole" — the Cutter's concern, not the Fuser's; (5) the Fuser's intro promised the ⊕ letters travel with a fused output, which carries none (they travel with a cut or split output, the only direction that needs them); (6) each window reads the sidecar at Probe, so groups changed in the Fuser reach the Cutter only after a re-Probe — documented; (7) a saved layout holding the old Model Workshop drops it once, the class being abstract — noted in the changelog. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Self-review of 48d323b, fixed in the commit above:
Not changed, considered: both windows open on the same source each run their own Blender probe to the same preview FBX path — sequential, and each keeps its own instantiated preview, so no conflict. 🤖 Generated with Claude Code |
…s each shell under its group's letter "The Fuser -> Cutter handoff loses group letters": Fuse wrote only the source's sidecar, so a fused output opened in the Cutter knew no groups and a cut shell's pieces inherited nothing. Every fuse Result now names its shell (node index and name), and Fuse writes <output>.fuse.txt from them: one line per fused group under the shell's own index, and the unchanged lines of a group that produced nothing (the fuse never renumbers nodes). WorkshopRules.FusedOutputSidecarLines, tested both ways (written, then read back by the resolver); the parallel-fuse test checks each result's shell against the output's node table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addressed in the commit above: Fuse now writes the OUTPUT's sidecar as well — one line per fused group under the shell's own node index and name (every fuse Result names its shell), and the unchanged lines of a group that produced nothing. A fused output opened in the Cutter knows its groups; a cut shell's pieces inherit the letter; the Fuser welds them back if asked. Rule: WorkshopRules.FusedOutputSidecarLines, tested written-then-read-back; the parallel-fuse test checks each result's shell against the output's node table. 🤖 Generated with Claude Code |
…groups' letters and counts fit User: "make the fuse button twice as high so the text of all the groupings can fit better." 56 px and a word-wrapping button style (built lazily from GUI.skin inside OnGUI). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…user 2026-09-18) ModelSplitterWindow, Tools ▸ HAF ▸ Model Splitter, and every mention in the window, the rules, the docs, the quickstart and the changelog. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the sidecar, so groups saved in the Fuser reach the next cut The letters kept across a re-Probe are the user's edits — in the Fuser. The Splitter never edits a letter: its letters are the sidecar's, yet a re-Probe kept the stale ones in memory while the docs told the user to re-Probe after changing groups in the Fuser. The Splitter now reloads the sidecar on every Probe; the Fuser keeps its edits as before (Load groups is its explicit way back). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addressed in the commit above: a re-Probe in the Splitter now re-reads the sidecar. The letters kept across a re-Probe are the user's edits, which only the Fuser makes; the Splitter never edits a letter, so its letters are the sidecar's and a re-Probe reloads them — groups saved in the Fuser meanwhile reach the next cut, as the docs say. The Fuser keeps its edits as before, with Load groups as its explicit way back. Docs and changelog now describe the two behaviours precisely. 🤖 Generated with Claude Code |
…p, in both windows; Insert marks for split User (2026-09-18): "an easy way to mark a unit for removal with the Del key", "also possible with a combobox (split up or delete)", "Insert should mark it for split-up", "Delete should also be active in the Fuser". A marked part loses its mesh in the output — what a fused source loses — and keeps its transform and children; the mesh data is left in the file as an orphan the bake never reads. Applied after the window's own work by Split, Plane cut and Fuse alike (none renumbers nodes, so the marks' indices hold). The Splitter's row popup is "– / Split / Delete" (a whole part offers no Split), the Fuser's letter popup ends with "✕ Delete"; Delete toggles the mark in both windows and clears the others, Insert marks for split, – / 0 / Backspace clear every mark; "Show only" lists "Marked for deletion". The mark is the window's, like a Split check. GlbDisconnectedParts.RemoveMeshes, tested: the marked node loses its mesh and keeps its transform, the rest untouched, a meshless or missing node is reported and nothing written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…>.marks.txt, shared by both windows User, after marking 144 chain links: "but then how is the delete state persisted?" They were the window's only, lost on a close or a domain reload, while the ⊕ letters had a sidecar. Now they have one too, in the fuse sidecar's format with S for split and X for delete, so the same resolver reads it by node index and name. Save marks / Load marks in the Splitter; Save groups / Load groups in the Fuser carry it as well; every Split, Plane cut and Fuse writes it beside the source; the first Probe of a file reads it, and a re-Probe keeps what is in memory (as the Fuser keeps its letters). Both windows share the file, so a deletion marked in the Fuser reaches the Splitter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The Split checks and the deletion marks now persist together in 🤖 Generated with Claude Code |
…now reads before Preserve User: "could you switch 'Preserve reduce' and 'Detail reduce' and add 'Default reduce'." Preserve's dial moves below Detail's — the exception reads last. The new dial cuts every part still marked Default (nothing chosen for it) exactly as Body parts are cut, so a model whose thousand small fittings are all undecided slims in one move. It joins the tier summary, the Verify projection, the geometry-work gate, the fast-path inert notice, the reset and the recipe DTO. The rig script takes it as a TAGGED argument (defaultreduce=@<names file>|<percent>) because the positional block is full at argv[75]: an older script with a newer Lab ignores the tag, a newer script with an older Lab reads 0, and a recipe that never touched the dial produces a byte-identical command line. Both copies of vehicle_rig.py are in sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e must not clear the groups file Three findings from reviewing the branch against the running code: 1. The Fuse button now fires with deletion marks alone, and the write path still called WriteFuseSidecar unconditionally. An empty write CLEARS the file (Save groups' documented "clear"), so a delete-only run silently dropped the ⊕ groups saved earlier. It is written only when a group actually fused. 2. A `BackSlash` const had crept into the Vehicle Lab: scaffolding from the patch script that wrote the Default tier, where every other line in the file spells the separator directly. Gone. 3. The Default names file was written on every Generate — thousands of lines for an undecided model — even with the dial at 0. Written only when the dial asks; the tooltip now says the tier costs real Blender time on a model with thousands of shards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… restart User: "why did the Model Splitter and Model Fuser dialog disappear after startup, it seems their position is not persistent." Unity restores a docked editor window by binding its saved layout entry to the MonoScript whose FILE NAME matches the class; both windows were nested in ModelWorkshopWindow.cs, so the binding failed and each dropped out of the layout on the next restart. ModelSplitterWindow.cs and ModelFuserWindow.cs now hold one class each (with their .meta), the base window keeping its own file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Critical self-review of the branch, findings and fixes. Real defects, fixed
Noise, removed
Considered, deliberately left
🤖 Generated with Claude Code |
…ur a metre above a roof is not its skin The Romanic's bridge deck (user: "a transparent hole on the bridge deck — did we forget to mark something, or is this another fusion issue?"): a fusion issue. A 39-face roof region on each side, facing up and correct as authored, welded into a sheet with the deckhouse and found 74 "twins in front" — the deckhouse's undersides a metre and more above — and the double-skin rule turned the roof over while the sheet's own inside-out score read +0.55, plainly outward. Found by ray-casting the fused output from above and tracing every hole cell back to the source part that authored it (drill_holes_src.py): 230 cells in group D, 211 of them roof faces that face UP in the source. Measured on the ship, every genuine double skin has its twin within a third of the old 1 % reach — the hull skins at 0.03-0.34, the deckhouse skins at 0.05-0.13, the test fixture at 0.20 — while the roof's neighbours sat at 0.72. Half the reach keeps every skin and loses every neighbour (a deck and the ceiling below it sit at 1.4 %+). Precedence untouched: a confident score outranking the twins was tried first and broke the double-skin fixture, where the twins are right and the score wrong with the same signature. Holes from above over the whole ship: 339 cells → 126, group D 230 → 17 (the 17 left are authored inside-out in the source, 35 there); the Teutonic's fused hull 70 → 49, its other shells and every per-group number unchanged. Each sheet's report line now names its parts and how far and how straight its twins lie — the two numbers that found this. Test: a roof with a downward ceiling 0.7 % of the length above it keeps its winding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The bridge-deck hole (user report) — a fusion defect, fixed in 50a334f. Found by ray-casting the fused GLB from above and tracing every hole cell back to the source part that authored it: group D had 230 hole cells, 211 of them roof faces of Object_6 / Object_1774 that face UP in the source. The sheet holding them read Measured across the ship, every genuine double skin has its twin within a third of the old 1 % reach (hull skins 0.03–0.34, deckhouse skins 0.05–0.13, the fixture 0.20). The reach is 0.5 % now: every skin kept, every neighbour lost. Changing the precedence (score before twins) was tried first and broke the double-skin fixture, where the twins are right and the score wrong with the same signature.
Teutonic per-group numbers unchanged. Each sheet's report line now names its parts and how far and how straight its twins lie — the two numbers that found this. 854 tests (new: a roof with a downward ceiling 0.7 % above it keeps its winding). 🤖 Generated with Claude Code |
…ene and one glTF validity gap Reading the branch diff end to end after the reach change: 1. Two comments had been glued onto the wrong line by patch scripts that matched a line's prefix: the IslandLines comment sat on the FusedNodeIndex line, and the old FuseLabels history sat on the Splitter's labels. Each is back where it belongs. 2. The twin distance and straightness were still labelled DIAGNOSTIC; they are the report's, with the reading a skin gives (≤ 0.35 of the reach, ~0.9 straight) against a neighbour (0.7+) spelled out. 3. The twin-rule comment named a variable that never existed (`skinReach`); it is `twinReach`. 4. RemoveMeshes stripped a node's skin with its mesh but not its morph `weights`, equally invalid on a meshless node. Gone too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Critical self-review, second pass — the whole branch diff against master, read after the reach change. Fixed in the commit above (no behaviour change except 4)
Checked and sound
Known and left
🤖 Generated with Claude Code |
…de regression, and the missing per-node test
Three findings from a critical review of this branch, all safe to take
on their own: none of them changes a single pixel of the preview.
- The filter block's comment was left half-replaced ("The flat-surface
and visibility filters / the flat filter came over on 2026-09-18"):
ungrammatical and self-contradicting, the same glued-comment class
flagged in PR #63. Rewritten as one sentence; the stray blank line
where the PreviewDir constant was removed is gone too.
- The accessor layout cache turned a DECLARED byteStride of 0 into
"absent" and silently fell back to tight packing, where the old
per-read path rejected it ("bufferView byteStride is smaller than its
element"). glTF forbids 0, so no real file hits it, but the cache was
supposed to be behaviour-identical. Absent is -1 now; a declared 0 is
rejected as before.
- Nothing tested the thing that started this work: the preview must
give every mesh-carrying node its OWN entry, because the row
highlight keys on the node index (a file naming all 113 of its nodes
"Material2" lit the whole ship when the match was by name).
ExtractAll is now asserted to return one entry per node, each
matching ExtractPart's geometry, on a fixture with two nodes sharing
one mesh.
Left OUT deliberately: the winding swap for mirrored nodes. It is what
made the user's ship see-through, and the preview's handedness has to be
settled first. Also open from the review: the preview holds the whole
model in memory with no triangle cap, and the old *_wprobe.fbx assets
nothing references any more are still in the project.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
The Model Workshop is now two windows on one implementation: Model Splitter (
Tools ▸ HAF ▸ Model Splitter) and Model Fuser (Tools ▸ HAF ▸ Model Fuser).ModelWorkshopWindowis abstract with one flag,Fusing; the two windows derive from it.Shared by both: the file pickers, Probe, the filtered part list (verts, size, height, side, flat parts, Show only), the preview, "Find the mirror of …" and the ↑/↓ sweep.
_split/_cut_fusedThe one-step Generate (fuse AND split) went with the split. Chain the outputs instead: the ⊕ letters travel with a cut or split output (
WorkshopRules.TransferLetters), and a fused output's sidecar names each shell under its group's letter (WorkshopRules.FusedOutputSidecarLines), so cut-then-fuse and fuse-then-cut both work. The Fuse button is twice as high and wraps, so nineteen groups' letters fit.Marks persist — the Split checks and the deletion marks go to
<source>.marks.txtbeside the fuse sidecar (S= split,X= delete, the fuse sidecar's format and resolver), written by Save marks in the Splitter, by Save groups in the Fuser, and by every Split, Plane cut and Fuse; read at the first Probe of a file, and by Load marks / Load groups. Both windows share it, so a deletion marked in the Fuser reaches the Splitter. They are part of the pipeline, like the ⊕ letters.Mark for deletion (both windows): a marked part loses its mesh in the output — its transform and children stay, the mesh data is left as an orphan the bake never reads — applied by Split, Plane cut and Fuse after their own work (
GlbDisconnectedParts.RemoveMeshes, tested). A deleted part is neither split nor fused.Also in this branch (Vehicle Lab, so you can test everything from one checkout): a Default reduce (%) dial — the catch-all tier that cuts every part still marked Default, exactly as Body parts are cut — and Preserve's dial moved below Detail's, the exception reading last. The rig script takes the new tier as a tagged argument (
defaultreduce=@<file>|<pct>), so an older script and a newer Lab still run together, and a recipe that never touched the dial produces a byte-identical command line. Say the word if you would rather have this as its own PR.Test plan
bash tools/check.shpasses (853 tests; the Roslyn editor-compile step covers the window).Tools ▸ HAF ▸ Model Splittershows checkboxes, plane cut and Split only;Model Fusershows ⊕ popups, weld, Save/Load and Fuse only. Probe the Romanic in the Fuser: the sidecar's letters load, Fuse writesss_romanic_fused.glb. Open that in the Splitter: the letters are still on the rows (hidden), a split writes_splitand its sidecar.🤖 Generated with Claude Code