Upstream 7f629b8c'yi al (55 commit) + dikey açıklık ve sürükleme kapısı - #41
Merged
Merged
Conversation
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * fix: reconcile room surfaces with wall topology * fix: index room topology reconciliation * fix: address topology reconciliation review * test: broaden topology equivalence coverage * test: cover customized surface split in editor * style: format wall topology regression test --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(editor): add Streetscape plugin to Pascal Register @pascal-app/plugin-streetscape (pinned by commit) alongside the trees and mint plugins: plugin discovery + host panel at bootstrap, Next transpilation, and Tailwind @source so its panel styles compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): credit the Streetscape plugin's actual author Override creator/pluginUrl at registration — the upstream manifest points at a pascalorg repo that doesn't exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(editor): bump Streetscape pin to webp thumbnails Upstream converted the 37 PNG catalog thumbnails (~17 MB) to webp/svg (~630 KB), bringing the packed CLI back under its release budget, and fixed pluginUrl/repository metadata — keep only the creator-name override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…calorg#630) * fix(editor): keep non-terrain walls plane-bound on creation The terrain drafting flow stamped the ghost's explicit height and a supportOffset onto every wall whose frozen construction plane sat above y=0 — i.e. any wall started on a slab or deck, not just ground-hosted terrain chains. Such walls stopped following the level height and no longer re-elected their base from slab support. Gate the stamping on a ground-drafted plane (the terrain exception), and re-resolve the aimed support surface per commit for non-ground chains so a later segment still elects the slab it visibly crosses instead of being capped at the first click's elevation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nodes): rework wall panel around plane-bound top Top: "Follows level" (no explicit height, shows the resolved height) vs "Custom height" (seeded from the effective height so geometry doesn't jump on detach). Bottom: the terrain infill toggle no longer materializes an explicit height, so toggling it can't silently detach the wall top from the storey plane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): keep flat-ground wall drafts plane-bound Pointing at bare ground freezes a GROUND construction plane, and the commit path treated every ground-preferred draft as a terrain chain: stamped ghost height, persisted ground host, election capped at the draft plane. On flat ground (no sculpted terrain) all three are wrong — the wall showed "Custom height 2.50" instead of following the level, and a slab drawn later could never lift it, leaving the wall buried in the slab (z-fighting band at the base). Gate the terrain exception on terrainSupportLift(): ground-preferred drafts with no terrain support drop their draft options entirely and commit plane-bound. The terrain-freeze test now seeds a real terrain field; a new regression test pins the flat-ground shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes): restore full plane-bound defaults from the follows-level toggle Switching Top back to "Follows level" only cleared the stored height, so regression-era walls (stamped ground host + draft offset) kept their base pinned at the level floor, still buried in any slab. The toggle now also drops the draft offset, and drops a ground host when no sculpted terrain supports it — giving existing broken walls a one-click repair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes): carry the base repair through the bottom Auto toggle The follows-level toggle repairs regression-era walls, but walls whose Bottom already displays "Auto" while secretly ground-pinned had no clickable path that kept their custom height — the pin isn't reflected in the control, and the base stayed buried in the slab (z-fighting its side faces). "Auto" now performs the same re-election repair (drop the draft offset, drop a terrain-less ground host), and the segmented control fires on already-selected clicks, so clicking "Auto" itself heals the wall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(core): self-heal buried ground pins in the vertical canonicalization Regression-era walls carry supportSlabId 'ground', which short-circuits slab election: the base stays at the level floor, buried in the room slab and z-fighting its side faces, while the panel truthfully-but- uselessly displays the base as automatic. Users shouldn't have to click anything: the load-time canonicalization now strips the pin (and draft offset) when the un-pinned election would land the wall on a slab its base is currently embedded in. Deliberately narrow so the one legitimate flat-scene ground pin survives: a wall kept on the ground under a hovering deck is not buried (the deck's occupied interval sits above the base) and keeps its pin, and scenes with sculpted terrain are skipped wholesale — the pin is load-bearing there and the live terrain field isn't visible to this pure, authority-shared pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pascalorg#639) Viewport and area crop modes captured at the raw canvas backing-store resolution, so retina/5K displays produced multi-MB WebP snapshots that blow past upload transport limits. Clamp the output long edge to 2048 (matching the 1920-class standard presets) in the snapshot pipeline, the non-pipeline fallback, and the capture HUD resolution chip. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ries (pascalorg#641) A noPanel tab highlights in the rail and activates without opening the sidebar panel column, for host surfaces that swap the stage instead (e.g. the hosted item builder). Mobile layout skips noPanel tabs. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rg#642) Level height, wall custom height, ceiling custom height, and column height were all UI-capped at 6m, which rules out tall single-space builds (climbing gyms, warehouses, halls). 20m covers the tallest realistic single-storey uses — lead-climbing walls top out around 15-18m — while keeping the sliders usable in the common 2-4m band. The caps were never enforced in core schemas; only the controls change. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…org#643) * fix(editor): derive the wall placement box from the wall-local point The placement wireframe and the item it previews were computed from two different quantities: the node position snapped the hit in WALL-LOCAL coords, while the cursor snapped the raw WORLD hit independently per axis. Those are different lattices — wall-local X runs from `wall.start`, and wall-local Y is measured from the supporting slab's elevation, not world zero — so the box drifted off the item by the slab elevation plus up to a grid step, and the commit (which follows the node) landed where the box was not. `calculateCursorRotation` also returns the wall face's yaw + π, which is invisible for the z-symmetric door/window boxes it was written for but put the asymmetric `wall-side` item box on the far side of the wall. The coordinator already compensated for that π when publishing the 2D floorplan preview; that workaround goes away with the cause. Both now come from `resolveWallPlacementPose`, which maps one wall-local point through the hit object's frame — the exact inverse of how `localPosition` was measured. `z` follows the hosting convention rather than the hit depth, so `wall-side` items store the face they mount on (matching ItemSystem's per-frame push, no first-frame pop) and `wall` items stay centred in the thickness instead of being snapped out of a thick wall. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(editor): stop floor-stacking hosted nodes during a drag `FloorElevationSystem` documents that it "respects `floorPlaced.applies` so items with `asset.attachTo` (wall / ceiling mounted) are left alone", but it never called the predicate — only `getFloorPlacedElevation` did. For an opted-out node that resolver returns 0, so the system's write degenerated to `mesh.position.y = position[1]`. Harmless while `position` is the node's own host-local store value, but tools publish live transforms in WORLD space: the moment a wall item started moving, the system copied its world Y into the mesh's wall-local Y slot every frame and the ghost floated off the wall by the host frame's elevation. Hence the reported sequence — correct on `wall:enter` (no live transform yet), wrong throughout the move, correct again after commit (the coordinator clears the live transform first). Honour `applies` before the write. Covers the other three opt-outs too: ceiling items, cabinet modules inside a run, and non-floor duct terminals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…one editor (pascalorg#649) * feat: wire Bones plugin (structural framing) into the standalone editor Third-party plugin by @Snoopy147 — github:Snoopy147/plugin-bones. Registered like streetscape/mint: static import in bootstrap, transpilePackages entry, pinned-commit dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: Bones → pascalorg/plugin-bones — the full engineering X-ray All systems online: wall framing, CMU, floor, roof, foundation, electrical, plumbing, HVAC, jurisdiction profiles (51 states), takeoff. 191 plugin tests green; verified live in this editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: Bones verified build — hip rafter orientation, switch guard, skip-override plumbing seam Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump Bones pin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to e1ee7c0 (all 8 systems at LOD 400 build) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to ba4d5af (X-ray depth fix + flush foundation corners) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to 1edd562 (depth-clear sentinel X-ray) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to cda167b (WebGPU-safe depth-wipe X-ray) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: Bones ships opt-in — defaultInstalled false, enable per scene from the Plugins panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to c61669e (round-3 build: all round-2 blockers) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: lockfile refresh for plugin-bones c61669e Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to 1bb10bb (round-3 fixes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to f79efd4 (round-4 fixes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to 7c2801d (round-5 fixes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to c99a3c5 (round-6 fixes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to fd78253 (round-7 fixes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to c5a3606 (round-8: 90-degree girder fix) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump plugin-bones to a480fd8 (round-9 fabrication exactness) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: sort the plugin-bones import Biome's organizeImports assist wants it after @pascal-app/nodes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Julien Brissonneau <julien.brissonneau@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…pascalorg#650) * fix(editor): drop the stale 6 m caps left over from the 20 m storey raise `pascalorg#642` raised the room-envelope caps from 6 m to 20 m but missed two sliders, so a tall storey is still unusable in practice: - ceiling panel's non-level-parent fallback stayed at 6 m while `definition.ts` uses `POSITIVE_INFINITY` for the same case — the panel and the 3D drag handle disagreed on the same bound. - slab elevation (Surface/Floor and Base/Rim) stayed at 6 m, so a slab could not be raised past 6 m inside a 20 m level. `clampSlabElevation` in the write path is the real bound; the slider was just a UI ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(editor): make the ceiling's level constraint visible instead of silent A ceiling's cap is the storey plane (`getCeilingClampBound`), so on a stock 2.5 m level it is 2.49 m — which means the "Standard (2.5m)" and "High (3.0m)" presets clamped silently and read as dead buttons, with nothing pointing at the level height as the real gate. Presets taller than the level are now disabled with the available height in their tooltip, and the Height section names the bound. The clamp itself is unchanged: a ceiling still never pokes through its own storey. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… engine fixes) (pascalorg#653) Rendering: the X-ray now renders as a solid scene-layer pass plus a ghost overlay pass — it survives orbit/zoom (the depth-wipe approach silently never worked in the TSL pipeline) and no longer paints over foreground objects like nature-plugin trees, which now occlude and shade the skeleton like real geometry. Engines: repo-wide interpenetration gate + the geometry it caught (ridge-face rafter bearing, fascia outside tail cuts, dropped gable ends, sistered ties, wall corner/tee run insets, oblique foundation corners, per-bay floor blocking, live girder end-bearing validation, d/4 birdsmouth cap, purchasable 1x8 fascia, CMU grout field, HVAC square-trunk takeoff, LA-1/GA-1 circuit pins). 408 tests + tsc green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… copy) (pascalorg#655) Adds the 'Save full plans' button at the bottom of the Bones sidebar — a print-ready LOD 400 plan set (per-system plan sheets + schedules, Print -> Save as PDF). Trims the catalog description to stay high-level. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ay, mitered blueprints, exterior detection) (pascalorg#656) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ough openings (pascalorg#657) Prod-reported bug: wires bored through low windows, boxes on doors. Routing now detours over headers/under sills for ANY opening crossing the drill plane; anchors/junctions/switch/panel snap out of ROs. Gated by electrical.openings.test.ts + visually verified (QA E1 PASS). Also: blueprint round-1 follow-ups (shared sheet transform, circuit-ID labels, GEN palette spread, callouts, even pagination, link-wall footings). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…org#658) Two-storey prod bug: the roof (and any cross-level system) rendered at ground level. Members are level-local; cross-level roofs now shift by the host's exact storey stacking (per-building baseY incl. baseElevation, default height 2.5). One X-ray per building frames ALL roof-bearing levels; per-building ground detection and floor-to-floor datum. Verified by adversarial code review (two rounds, all findings fixed) + in-browser alignment with the host-drawn shell to the pixel. 457 tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ode mounting) (pascalorg#659) * fix(viewer): frame-rate-independent level elevation smoothing + bones 4cd28a0 The storey lerp factor delta*12 exceeds 1 past ~166 ms frame times — the recurrence then AMPLIFIES the error each frame and levels oscillate kilometers off-screen (exploded mode 'not separating', fit-camera chasing a runaway level = blank viewport). Proven by per-frame instrumentation: y series 0 → -179.8 → -587.9 → peak ±1309 m under load, while a light scene damps 11.6 → 7.500. exp(-12*delta) keeps the factor in (0,1) for any delta; converged values snap exact. Bones 4cd28a0: cross-level roof framing mounts into the roof LEVEL's Object3D (tagged level-local members, no baked storey offset) so stacked/ exploded/solo apply natively — trusses ride the roof level's exploded slot and hide when soloing other floors. Plus electrical RO edge clearance for the panel and Alpha branding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * clamp instead of exp: preserves the exact-snap contract at delta>=1/12 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ctural set + characteristics (pascalorg#660) Placed-fixture plumbing (meter, water heater, hot/cold homeruns, DFU-sized sloped DWV to a sewer exit) hardened through six adversarial verify rounds (14 defect classes fixed + gated, incl. cross-trade separation and rough-opening avoidance). Blueprint set grows cover/elevations/section with round-2 examiner fixes. Building-characteristics drawer + CSV + sheet block. Under-slab DWV ghosts through the floor in X-ray. 513 plugin tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…H/water/sewer/power) (pascalorg#661) One-click 'Place service points': five labeled, selectable, movable equipment nodes (panel with bolt sign, WH, water valve, sewer stub, power weatherhead). Wires and pipes re-derive around wherever they move; overrides are authoritative with NEC 110.26/RO warnings; two adversarial verify rounds (8 defects fixed + gated). 559 plugin tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ded connectors (pascalorg#662) Sections with true cut poché (filled plane-slices, legible on oblique geometry), width-aware circuit-label de-collision, grid-based roof coverage flag, full flag-list wrap, butt-cap linework; braided supply connectors for off-wall fixtures (RO-flagged, parts-counted). Examiner round-3 fix-check + connector skeptic PASS; 585 tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ter after mount (pascalorg#665) * fix: re-derive selectable-kind subscriptions when plugins register late Plugin kinds register through async dynamic-import discovery, AFTER the selection managers mount and snapshot getSelectableKinds() into their emitter subscription lists. In prod (community bootstrap fetches plugin chunks over the network) a plugin node kind therefore never gets hover / click / double-click subscriptions unless an unrelated effect dep happens to change — user-visible as "windows outline on hover, plugin service boxes don't". Fix: the registry bumps a monotonic version + notifies listeners on every registerNode/_reset; a new useRegistryVersion() hook (useSyncExternalStore) feeds that version into the dependency arrays of every effect that snapshots the kind list (5 in the editor SelectionManager, 1 in the viewer SelectionManager), so a late plugin load rebuilds the subscriptions. Gates: registry version bump + listener notify + unsubscribe, and loadPlugin notifying per kind (packages/core registry.test.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * read registryVersion in effect bodies (biome exhaustive-deps) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * biome format Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…le exterior, attic HVAC, service UX (pascalorg#666) Exploded view reads floor/trusses/shell as strata; gable walls on roof levels wear the full exterior stack; ducts route in the attic with ceiling grilles (never through plate bands — new M1 gate); thermostat, heat pump and electric meter join the movable service points (street → meter → panel cable on the wall graph, RO-aware); door-style drag + per-wall engineering card; five verify rounds, every confirmed finding fixed + gated. 668 tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nspector) (pascalorg#667) * feat(core): plugin inspector-card extensions in the registry Plugins can now contribute sections to the floating node inspector card: `Plugin.inspectorExtensions` declares { id, pluginId, kinds, icon, title, component } entries; `loadPlugin` fans them out into a per-kind map read via `getInspectorExtensions(kind)`. Same-id re-registration replaces in place (dev HMR), the test reset clears the map alongside `pluginIdsByKind`, and registration bumps the registry version so late-loading plugins re-render open inspector cards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): render plugin inspector extensions on the floating card The inspector card wrapper self-resolves the single selected node (viewer selection + scene nodes — customPanel kinds render their own PanelWrapper, so this is the one spot every card flows through) and, for each registered + installed InspectorExtension of that kind: - folded card: a 16px icon button in the header, left of the collapse chevron — click expands the card straight to the extension's section; - expanded card: a collapsible PanelSection after the kind's own controls (lazy + Suspense + WeakMap-cached like resolveCustomPanel, wrapped in an error boundary), auto-expanded when jumped to via the header icon and collapsed again once the card folds. Install gate mirrors use-plugin-panels (useScene installedPlugins); extension lists re-derive on the registry-version bump so async plugin loads surface on already-open cards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ontrols (pascalorg#668) UX correction to pascalorg#667 after user review: clicking an extension's header icon used to uncollapse the card and APPEND the extension as a section after the kind's own controls. The card now has two mutually exclusive expanded modes: - chevron / header press → ONLY the kind's regular controls (no extension sections appended); pressing again folds as before; - extension icon → ONLY that extension's content fills the card body (its own section chrome), regular controls hidden. The icon shows an active (cyan) state while its mode is on; pressing it again — or the chevron — returns to the regular controls without folding. The transition table lives in lib/inspector-card-mode.ts (pure, gated by mode-toggle tests encoding the either/or contract, incl. the stale-id fallback to regular when the selection kind changes or the plugin is gated off). Collapse semantics, drag, the shared desktopInspectorCollapsed reset machinery, the installedPlugins + registryVersion gates and the lazy/Suspense/ErrorBoundary loading are unchanged. The mobile sheet (no header icons) keeps pascalorg#667's appended sections. Core InspectorExtension docs updated to the new contract. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…eering card, mixed CMU/framed walls (pascalorg#669) Three-layer exploded view works with the X-ray on the roof level (render- only mount tag; elevations unaffected); wall-card Engineering section (either/or modes via pascalorg#667+pascalorg#668); mixed CMU/framed walls: course-snapped seam, PT sill + R403.1.6 bolts, zoned openings, corner butt joints incl. width-aware acute retreats — six verify rounds across the batch, every confirmed finding gated. 721 tests green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-3 verify round (pascalorg#670) * chore: bump plugin-bones to dc1daf3 — cladding both-modes fix + verify round S7/S8/S9 - Cladding choice now reads in BOTH render modes: per-family X-ray member colors (brick/EIFS emitted NOTHING before) + solid-mode wall repaint via slots.exterior (library textures / minted flat scene materials). - Batts compress into the layer cavity (0.15m zone-5 interpenetration fix) with an aggregated 'compressed' flag; INTL R fallback parity. - Colinear dedupe now merges duplicate twins' openings (studs framed around doorways that only the dropped twin carried). - Explicit stud overrides too deep for the drawn wall warn on both Engineering surfaces. - Plugin dev-dep types bumped to @pascal-app/*@0.9.2 (slots API). 779 plugin tests + tsc green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bump to f308cf36 — verify round 2: air gap spacing, misfit tolerance, single-undo cladding pick Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ndensers, cavity-fit framing (pascalorg#676) * chore: bump plugin-bones to 05ca742 — night-4: movable outlets, AC condensers, cavity-fit framing, E4 - Movable outlets (user ask Q7): every derived receptacle/switch is hoverable + draggable along its wall like a door; code-aware snapping (RO avoidance, stud-adjacent mounting with blocking members, height clamps, NEC 210.52 spacing advisory); wires re-route on release. - AC condensers (user ask): 1-N units sized by conditioned area with a climate-zone divisor, concrete pads outside an exterior wall, refrigerant line-sets to the air handler, NEC 440.14 disconnects. - Cavity-fit framing: S1 same-wall interpenetration class retired — framing geometry compresses to the drawn wall (labels/takeoff stay nominal), one aggregated flag per class. - E4 closed: no wire crosses room air at living height — island hops cross above EVERY wall; buried island feeders. - Selected wall exempt from the dollhouse cut (finishes readable from any angle, dedupe-twin aware). 855 plugin tests + tsc green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bump to 6e7dcfa4 — batch-round fixes (pad alignment, twin overrides, batt notching) + outlets experimental flag (dash ids, seeding off by default) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bump to 8f40d06b — truthful unframed fallback + non-vacuous F3 gate (flag-gated paths) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… seed parity, disconnect RO slide (pascalorg#677) - AC condensers get their dedicated circuits: panel → disconnect homeruns (30A/10AWG ≤3 tons, 40A/8AWG above) with honest schedule + legend ratings and a collision-free circuit color family. - Heat-pump service node seeds at the engine's slid anchor (sign stands ON the unit); inside-corner and on-wall-anchor edge cases guarded. - The disconnect slides clear of fronting window/door ROs (was: box mounted on the glass, silently); unclearable cases warn. Loop-verified: two adversarial rounds; visual PASS at 859e156a (the delta to this sha is the numerically-gated color config + row text). 865 plugin tests, tsc clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * feat: add Blender-style custom mesh edit mode * fix: complete custom mesh edit mode * feat: refine custom mesh editing experience * fix: support elevated placement and refine mesh editing * fix: make surface placement registry-driven * feat(nodes): add custom mesh face materials * fix(nodes): refine custom mesh face materials * fix(editor): address custom mesh review findings * feat: improve custom mesh editing and material slots * feat: support items on custom mesh faces * fix(editor): reject wall attachments on sloped custom mesh faces * Rename custom mesh to block * Move block face placement behind face host capability * Add modal uniform scale for block editing * test(editor): keep the block face host registered across suites `registerTestBlockFaceHost` skipped registration whenever any `block` kind was already present in the shared node registry. The wall drafting suite registers its own floor-placed `block` stub, which has no `faceHost` capability, so when it ran first the face-host stub was never installed and every face placement resolved to null — 7 tests failing by file order alone. Renaming `custom-mesh` to `block` is what made the two stubs collide. Gate on the capability rather than the kind name, replacing a registered `block` that cannot host faces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Wassim SAMAD <wass08@gmail.com>
…tion (pascalorg#680) Selecting several nodes of the same type now shows one inspector for their shared parametrics.groups fields, with mixed-value controls, a single undo step, and wall/ceiling follows-level vs custom height. Co-authored-by: Cursor <cursoragent@cursor.com>
…g#682) * fix(editor): stop read-only sessions from wiping scenes with an empty autosave PUT Root cause of the 2026-08-16..18 scene-wipe class (a4993ec9f1ab, 1befee38f973, reproduced live): useAutoSave's store subscription attaches before the Editor's scene-load effect (hook order), so useHostPanels' mount-time default-installedPlugins sync marks the session dirty with zero user edits while the store still holds the empty pre-hydration state. The load effect then runs unloadScene(), whose transient 0-node write re-baselines the wipe guard to 0 via trackLoadedGraph. Any effect cleanup in that window (StrictMode simulated unmount in dev, tab close or navigation in prod) runs flushOnExit, which checked only the dirty flag — it serialized the empty store and PUT it with If-Match: 1, leaving v2 with 0 nodes. Defense in depth, all three layers: - use-auto-save: isLoadingSceneRef now starts true (autosave arms only after the first hydration completes), and the exit flush is decided by the pure decideExitFlush(), which skips any flush while a load is in flight — the store content in that window is transient, not user data. - scene-loader: tracks the server's known node count (initial meta, PUT responses, SSE events) and refuses to PUT a 0-node graph over a populated server copy, with a console.error; 409 empty_graph_rejected responses surface as a save error instead of the conflict banner. - PUT /api/scenes/[id]: rejects a 0-node graph aimed at a scene that has nodes with 409 empty_graph_rejected unless the caller passes force: true. A silent wipe is unrecoverable in place; an intentional full deletion is rare and still available via force (and every version stays in scene_revisions). Gates: decideExitFlush matrix incl. the exact traced wipe sequence, empty-graph-guard unit tests, and a route-level integration test running the real PUT handler against a temp SQLite store (409 body, store untouched after rejection, force path, empty-over-empty allowed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: seed the wipe-guard fixture through the raw store CI resolved '@pascal-app/mcp/operations' to a build without saveScene (turbo-cached dist) — the store's save() is the stable primitive the operations layer delegates to anyway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: guard file must sort before scene-store-server's module mock bun's mock.module leaks process-wide to later files in the worker — scene-store-server.test.ts stubs '@pascal-app/mcp/operations', which starved the PUT-guard fixture of saveScene/loadStoredScene in CI (single worker). Renamed to sort first + hazard comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: restore the real mcp modules after scene-store-server's mocks bun's mock.module poisons the registry for every later file in the process — downstream route tests saw a stub facade without saveScene/loadStoredScene in CI. Capture + restore in afterAll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: inject real store/operations via relative source imports Other test files' mock.module stubs on '@pascal-app/mcp/*' stick for later dynamic imports on linux — three CI runs starved the route fixture of saveScene/loadStoredScene while macOS passed. The fixture now builds a real SqliteSceneStore + facade from relative source paths (immune to subpath mocks) and injects them via a test-only setter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: biome format Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A wall hidden by the wall-mode pass ('down' mode, cutaway-hidden faces,
auto-mode interior-interior partitions) still raycasts at full height via
its invisible collision mesh, and the selection path stopPropagation's on
it — so clicks aimed at VISIBLE objects standing behind the hidden wall
(wall-mounted plugin device/service boxes in X-ray mode, items) select an
invisible wall instead. Night-5 evidence: arming a receptacle drag on the
demo scene's south wall selected wall_x86… two meters in front of it
(raycast hit at 1.03m vs the device at 3.93m), and the follow-up click
committed an accidental WALL move plus its auto slab/ceiling/zone sync.
WallCutout now stamps userData.wallHidden with the effective hide state,
and the wall renderer's pointer handlers early-return (no emit, no
stopPropagation) while it's set — R3F continues to the next intersection,
so hover/select/click pass through to whatever the user actually sees.
Delete mode keeps the events: hidden walls stay hover-targetable for the
deleteInvisible highlight flow.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… tee guards, examiner fixes (pascalorg#684) - Movable outlets default ON: every receptacle/switch drags like a door with exact single-undo (the drag-commit cascade that woke space-detection mid-commit is gone; pairs with pascalorg#683's pointer-transparent hidden walls). - Tee-junction guards: parallel-wall false tees filtered, width-aware oblique retreats in all three consumers, junction honesty warnings. - Blueprint examiner round: circuit legend never truncates (second column), condensers tagged CU (not AH), GEN color walk no longer wraps into the lighting band. 878 plugin tests, tsc clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…pickable (pascalorg#686) * fix(editor): stop node tops lifting floor placements off the ground pascalorg#638 made the pointer support election consider node tops by default — `options?.includeNodeTopSurfaces === false` opted out, so every caller that passed nothing (item placement, registry move/presets, slab drafting) started electing them. It also gave `item` a `surfaces.top`, widening the candidate set from wall/item/column to wall, slab, ceiling, cabinet, column, item, shelf, block. A ray aimed at a floor crosses every upward-facing face above that floor first. In a finished room that is the ceiling: placing an item at the room centre elects the ceiling's top face (nearest hit, normal.y ≈ 1) and freezes it into the draft's authored Y via `resolveFrozenFloorPlacementPatch`, so the item sits at ceiling height instead of on the floor. Walls the ray passes over do the same in a narrower band. Restore the opt-in. Keep pascalorg#638's registry-driven discovery — the kind list is still derived from `capabilities.surfaces.top` rather than hardcoded — but only the tools that build ON a surface ask for it: wall (already did), column, fence, stair, block. Item placement, registry move and slab drafting go back to placing against the floor the pointer indicates. Also exclude the node the active interaction is placing or moving. Its mesh rides the cursor, so electing its own top would raise it by its own height every pointer move. The tools neuter the dragged mesh's `raycast` for their own pointer routing, which happens to cover this today — but that is each tool's private convention, and async-mounted item children are only neutered on the next frame. The election owns the invariant now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(viewer): keep batched walls answering the pointer pascalorg#608 sews a level's walls into one mesh once they settle (8+ walls, 180ms quiet). Each sewn wall is moved off SCENE_LAYER onto BATCHED_LAYER by `hideBatchedWall` so it costs no draw call while staying in the graph — with its R3F pointer handlers still attached. R3F picks with one shared raycaster whose default mask is SCENE_LAYER alone, so a batched wall stops being hit: no `wall:enter` (no hover outline, no paint preview), no `wall:move`, no `wall:click`. Selection is the circular case — a selected wall leaves the batch, but the click that would select it never lands. pascalorg#608 saw this for measurement and added `setSurfaceRaycastLayers` for the raycasters that module builds; the shared event raycaster was never opted in. Enable BATCHED_LAYER on it. Additive rather than `setSurfaceRaycastLayers`, which resets the mask — right for the private per-query raycasters it was written for, wrong for the one every pointer event goes through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
pascalorg#687) * fix(nodes/wall): let a hovered wall out of its batch so it can outline Hover feedback for a wall is an outline, nothing else: `SelectionMaterialSync` skips walls outright and the cutaway pass tints only a selection or a delete hover, so `default`, `paint-ready` and `paint-disabled` all reduce to `outliner.hoveredObjects` and a colour. `MergedOutlineNode` draws that mask with `renderer.render(scene, camera)` on the main camera, which enables no `BATCHED_LAYER`. Once pascalorg#608 sews a level's walls (wall mode `up`, 8+ walls, 180ms quiet) every wall on the floor is on that layer alone, so it reaches neither the depth pass nor a mask pass and hovering it lights up nothing at all. Selection looks fine only because `collectTintedWalls` already pulls a selected wall back out of the batch — the click lands, the wall starts drawing itself, and the outline appears. Hover had no such release, so on any finished floor the wall you point at goes dark: no hover outline in select mode, and in paint mode no preview of the surface the next click will paint. Release on any hover, not just a delete one. Same mechanism, one more reason: selection and delete tint through materials the merged mesh never reads, a plain hover outlines through a camera that never sees it, and both are fixed by the wall drawing its own geometry while it is lit. One wall is lit at a time, so the cost is the one extra draw call the delete path already accepted. Verified on a 12-wall level in the community editor: before, hovering a wall in `Full height` produced no outline in select or paint mode while `Cutaway` (where batching stands down) outlined correctly; after, both modes outline in `Full height`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: apply biome formatting to the hover test Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#651) * Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * feat: add wall-hosted lean-to roof extensions * feat: refine lean-to roof extensions * fix: refine lean-to wall and eave junctions * feat: automate roof gutters and downspouts * fix: address roof drainage review findings * fix: coordinate roof extensions with drainage * fix: complete lean-to extension controls * fix: complete lean-to extension behavior * fix: refine lean-to appearance and tree * fix: preserve resize values and restore roof icons * Fix lean-to shed roof side infill * Lock managed lean-to roof affordances * Use selection proxy for lean-to managed roofs * Bend curved lean-to extensions as a concentric annular band Represent the host wall's curve as a true-arc descriptor (center + radius) in each managed node's local frame, replacing the scalar sagitta that was re-derived through three synthetic chords and ballooned the deck into a filled disc. Every consumer — deck, gutter, downspout outlet, posts/beam/ rafters, and the 2D floorplan — now sweeps one shared concentric circle, so the canopy hugs the arc as a thin band for any extent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix curved wall shed geometry * align shed extension with architecture * fix lean-to corner joins and curved gutters * fix mesh editing snap configuration * fix floating corner post and rafter on curved lean-to joins Bend the shared corner post and corner rafter onto the wall arc like every other member, instead of placing them at raw flat arc-length coordinates. Identity for straight walls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix lean-to gutter/downspout edge cases from review Address Cursor Bugbot findings: curved-gutter downspout mapping and prescribed eave-Y, nearest-gutter reuse on segment refresh, asymmetric overhang reach in roof-edge attachment, manual-mode no longer silently reattaches, terrain/slab changes now re-trigger lean-to reconcile, and multi-node patches derive from the pre-patch node. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix biome check formatting and import order Resolve the quality CI failure: apply Biome formatter to the downspout and gutter-refresh edits and sort the lean-to system imports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * raise timeout on lean-to gutter corner angle sweep test The test assembles and renders a full gutter joint across ~127 angles, running ~5s and tripping bun's 5000ms default per-test timeout on slower CI hardware. Give it an explicit 30s budget; assertions are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…#690) * Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * feat: add paintable roof accessory materials * feat(editor): rotate roof direction while drawing * Fix roof face consistency and editing flow * feat: add automatic open roof valleys Clip entering roof segments against their host without removing host surfaces. * fix: complete roof intersection trimming * refactor: align roof features with architecture * test: align roof accessory paint expectations --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…st (pascalorg#692) * chore(core): evict the last runtime R3F import from the core barrel ElevatorRuntimeSystem (a useFrame wrapper) moves to @pascal-app/nodes' elevator system, its only consumer; stepElevatorRuntimes stays exported from core. A new architecture test fails core on any runtime three / @react-three import (type-only imports are erased and allowed) — proven red on a probe file with named, side-effect, and mixed type/value forms. Importing the core barrel from a Next.js route handler no longer evaluates R3F under the RSC server condition (the capture-upload 500 class of bug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(core): keep the registry barrel React-free useRegistryVersion moves its export from registry/index.ts to the core barrel: the registry barrel is reachable from server-safe graphs (schema → spatial grid → registry), and the hook's react import crashed Next.js route handlers that import @pascal-app/core/schema. Public API unchanged — consumers import it from '@pascal-app/core' as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…pascalorg#689) * fix(nodes/wall): hidden walls stay ray targets while a door/window tool is active pascalorg#683 made walls hidden by the wall-mode pass ('down' X-ray mode, cutaway faces, auto-mode interior partitions) pointer-transparent so clicks reach the visible device/service boxes behind their invisible full-height collision meshes. But the door/window MOVE and PLACE tools drive their whole cursor model from the wall pointer events those handlers emit (wall:enter/move/click/leave): with hidden walls silent, the ray falls through to the ground grid, grid:move free-follows, and the opening detaches from its wall as a red world-axis-aligned ghost ("rotates in place in red 90 degrees") instead of sliding along the wall. Before pascalorg#683 those drags worked in X-ray only by accident of the same defect that misfired outlet clicks. Fix: a counter-based hidden-wall pointer HOLD in core (holdHiddenWallPointerEvents / hiddenWallPointerEventsHeld). The four wall-opening tools (door + window, move + place) acquire it for their active lifetime; the wall renderer's gated handlers keep events flowing while a hold is live. No tool active means no hold, so plain selection clicks still pass through hidden walls — the night-5 D4 outlet fix is intact (MoveRegistryNodeTool never touches the hold; its cursor is the ground plane). Delete-mode hover keeps its existing exception. The gate predicate is extracted to wallPointerEventsSuppressed so the truth table is pinned by tests without an R3F rig. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: biome format Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ut (pascalorg#693) The sweep runs ~5.7s on a 2-core x64 CI runner — past bun's default 5s per-test budget (2-3s locally on Apple Silicon). First surfaced by private-editor's quality gate on the pascalorg#692 submodule bump, the first CI to ever run this suite. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gesture (pascalorg#694) * fix(nodes): door/window MOVE drags ride the node's own wall and undo as one step Two night-6 QA defects in the opening move tools, both fallout around pascalorg#689's hidden-wall pointer hold: 1. Wrong-wall capture: with every hidden wall a ray target, nearest-hit-wins let a hidden wall interposed between the camera and the dragged opening's own wall catch the wall:move stream — the drag rode a wall the user cannot see and the commit silently re-parented the opening onto it. New `shouldIgnoreWallEventForOpeningMove` gate (shared/opening-move-wall-gate): while MOVING an existing opening, a hidden wall may drive the drag only if it is the node's own wall (grab wall / current mid-drag host); visible walls always pass, so cross-wall re-parenting needs an explicit, visible target. Ignored events don't stop propagation, so the ray falls through to the own wall behind. PLACE / isNew duplicates keep the all-walls behavior. 2. Multi-entry undo per drag gesture: the tools paused history with a RAW `temporal.pause()`, invisible to the refcounted getSceneHistoryPauseDepth. zundo reads isTracking AFTER a write's subscribers run, so a cooperating system's balanced pauseSceneHistory/resumeSceneHistory pair (the space-detection sync, on any mid-drag reparent that touches wall children) zeroed the refcount and resumed tracking mid-gesture — the mid-drag writes became their own undo entries (transient states: commit fired at drag-arm, door hidden/reparented, orphan opening at the drop spot). New `beginOpeningMoveHistorySession` (shared/opening-move-history) holds the refcounted LEASE for the gesture and opens one deliberate tracking window (`commitStep`) for the drop write: mid-drag tracks nothing, drop records exactly one entry whose past state is the restored pre-drag baseline, cancel records nothing. Store-level tests pin the one-entry contract, the exact-baseline undo, lease composition, and the raw-pause leak this replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(nodes): biome format for the opening-move-history test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Live framing recompute during door/window drags (throttled effective- node computes riding the host's transient overrides), subfloor deck members + translucent under-layer on the floor sheet, composed header flags printed verbatim with flag-block pagination, engineered-header supplier SKU rows, LOD-400 B4 (one takeoff row per material + live climate-zone labels in all 51 states) and B5 (PT sole plates, R317.1), roof span discipline (B2), storey-relative elevation lifts, blocking full-extent cull tests. 950 tests at the pin. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ure behind them (pascalorg#697) * fix(nodes/wall): hidden walls join hover/selection raycasts nearest-first INVESTIGATION (user report: with the Bones X-ray on, mousing over a wall highlights/selects the furniture BEHIND it): (a) The hover + click-select path is R3F's per-mesh pointer events (useNodeEvents on each node's meshes -> mitt emitter wall:enter / item:click / ... -> SelectionManager's select-mode subscriptions, which set hoveredId / selection and stopPropagation). R3F delivers through the distance-sorted intersection list until propagation stops. (b) Why walls lost: the Bones framing renderer auto-switches the host wallMode to 'down'; WallCutout stamps userData.wallHidden=true on every wall; the wall renderer's pascalorg#683 gate then early-returned EVERY pointer event (blanket pointer transparency, no stopPropagation), so R3F fell through to the furniture behind. The framing members that visually occupy the wall's volume are handler-less InstancedMeshes (never raycast candidates), so nothing at the wall's depth could win. (c) SOLID / visible walls do NOT lose: wallPointerEventsSuppressed returns false for visible walls, the wall is the nearest interactive hit, and SelectionManager stops propagation on it — no ordering bug in the selection raycast itself. The defect is exclusively the hidden-wall blanket transparency. FIX: nearest-first with wall-furniture priority. A hidden wall's gated handlers now reduce the event's intersection list (extractWallSelectionRay) and handle the event unless something outranks the wall: - its own hosted subtree (doors / windows / wall-mounted children) at ANY depth gap — immune to grazing-angle inflation; - any non-wall hit at <= wallHit + 0.35m (devices flush/proud/recessed at the face, objects in front of the wall); - wall-MOUNTED hits further down the ray — non-wall hits within epsilon of another wall's collision hit (the pascalorg#683 night-5 D4 receptacle behind an interposed hidden wall keeps winning). Other walls' hits never compete directly, so parallel hidden walls can't both yield and drop the event into the room behind — the nearest wall wins by delivery order. Events without ray data fall back to pascalorg#683 transparency. Unchanged: delete-mode hover, the pascalorg#689 hidden-wall pointer hold (door / window MOVE+PLACE tools — pascalorg#694's own-wall gate keeps filtering those downstream), visible walls never suppress. Trade-off (pure host-side rule, no plugin presence flag): in a manual 'down' wall mode with no overlay rendering at the wall, the wall strip is hover/selectable again even though it draws nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(nodes/wall): reduce the pointer ray lazily — visible walls skip it extractWallSelectionRay walks the event's intersection list and each hit's parent chain; visible walls never suppress, so doing that per hover move over every visible wall was wasted work. Gate the reduction on the wallHidden stamp the predicate already consumes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nodes/wall): rank hidden-wall selection by hit OWNERSHIP, not raw depth QA f2 browser round falsified the premise that overlay meshes never reach event.intersections: R3F's event raycast recurses through the level / building wrapper groups (they carry pointer handlers), so Bones framing InstancedMeshes land in the list at the wall's own depth (probe6/probe7: d 4.246-4.422 vs wall 4.246) — all inside the epsilon tie-break, so the previous rule yielded everywhere framing renders and the click still fell through to the Double Bed. The wall's own RENDER mesh rides the same list at identical depth, which would have made the old hostedByThisWall subtree test self-defeating the moment walls build real geometry. Every hit is now classified by its NEAREST sceneRegistry-registered ancestor (selection-hit-owner.ts): - 'self-wall' (own collision/render/trim) -> neutral - 'other-wall' -> anchor only, never a direct competitor - 'selectable' (built-in selectable kinds + registry capabilities.selectable, e.g. bones:device) -> real competitor - 'passive' (bones:framing members, level/ building wrappers, zones, gizmos, grid, unregistered ancestry) -> never outranks Competitors win via: hosted-by-this-wall (any depth), <= wallHit + 0.35m, or within epsilon of another wall's hit (D4 interposed-wall receptacle). A hosted door resolves to the DOOR (registered deeper than its host wall), so 'self-wall' never swallows it. The reverse Object3D->id lookup rebuilds lazily off sceneRegistry.revision. This is robust standalone: even before the plugin-side raycast stub (plugin-bones d8bcc5d) lands, framing hits classify passive; any future overlay without the selectable capability behaves the same. Tests replay probe7 session B's exact hit shape (framing at wall depth + bed behind -> wall wins) plus the classifier truth table (self/other-wall, hosted door, framing passive, device selectable, wrapper/zone passive, deleted-node passive, revision-following lookup). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): walls can never stay degenerate — system resilience + self-heal QA f2 probe5/probe6: a scene LOADED with the Bones X-ray already active kept all 24 wall collision meshes as degenerate placeholder points with no wallHidden stamps and untouched base materials — geometry rebuild, cutout stamping, AND batching were all inert for 16+ seconds while other useFrame consumers (camera-controls) demonstrably ran. All three live in the ONE registry-mounted WallSystems bundle, so the failure is the bundle not running, not a per-wall skip. Toggling the X-ray mid-session (probe7) works, so the trigger is load-time mounting/ordering. Three fixes close the class: 1. RegisteredSystems re-derives its kind list on useRegistryVersion(). The list was snapshotted ONCE at mount (useMemo []), so any kind registering after that render — async plugin discovery, HMR — never mounted its system; a first render before ANY kinds register mounted nothing, permanently. Same staleness class SelectionManager already guards against. 2. Per-kind Suspense boundaries. One shared boundary meant any pending or failing lazy system chunk unmounted EVERY system (wall pipeline included) while it hung. 3. Wall self-heal sweep (wall-placeholder-sweep.ts, every 30 frames): any registered wall still on its mount-time placeholder geometry (stamped userData.placeholder, 3-vertex fallback signature) with no dirty mark is re-marked, so the normal rebuild path converges no matter what consumed the mount-time mark or when the system came up. The frame body now also reads the LIVE dirty set instead of a render-closure copy — a store-level set REPLACEMENT (scene load, plugin install) in the window before React commits no longer hides fresh marks. Walls now build their collision geometry regardless of initial visibility or load order — the invariant the hidden-wall selection gate needs to engage at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(viewer): biome import order for the wall-system sweep import Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): hovered hidden walls glow — the X-ray hover affordance QA f2 (REVISE): hidden walls became hover/selection ray targets (nearest-first), but an invisible wall gave NO feedback under the cursor — what the user saw lighting up on hover was the furniture BEHIND it. WallCutout now tracks the select-mode hovered wall (useViewer.hoveredId, hoverHighlightMode 'default' only, so the paint-preview snapshot/restore flows never interleave) and draws a hovered HIDDEN wall with a hover variant of its invisible stipple film: the same indigo emissive treatment as the wall selection highlight at a softer blend/intensity (0.28/0.07 vs 0.4/0.12), so hover reads as "this will select" and still steps up on click. Visible and translucent walls keep their existing hover affordance (the post-processing outline pass) — no double-highlight. Mechanics: the per-wall material choice is extracted into a pure resolveWallMaterialVariant truth table (delete > selection > hover > base, hover arm only for the invisible base) consumed via materialsForVariant; getSelectionHighlightMaterial generalizes into getEmissiveHighlightMaterial with per-variant cache + profile, adding getHoverHighlightMaterials. The hovered wall id joins WallCutout's highlightKey so hover changes refresh the material pass immediately. Known limit (pre-existing, noted per QA): the canvas cursor stays 'auto' over walls even in solid mode — not addressed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * tune(viewer): hover glow visible at normal zoom (QA: was ~2-5/255) The F2 browser round measured the hidden-wall hover glow at mean delta 0.13/255 over the viewport — functionally correct, invisible without A/B flipping. Blend 0.28->0.4, intensity 0.07->0.2 per the QA's 2-3x recommendation; selection emphasis (0.4/0.12) still reads stronger via its blend+dot treatment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fix: nearest-first hover/selection — hidden walls win over the furnit…
Lean-to roof extensions with automatic drainage, Blender-style custom mesh editing, synchronized 2D viewer modes, shared-parameter editing across a homogeneous multi-selection, plugin inspector-card extensions, an empty-graph save guard, a batch of wall hover/pick correctness fixes, and the autosave fix that stopped scenes being wiped during the load window. Thirty-five files conflicted; five of them were not real conflicts. `integration` carries cherry-picks of upstream pascalorg#607, pascalorg#608 and pascalorg#638, so git saw two independent additions of the same path. Four were byte-identical to the commit they were picked from and the fifth differed by one defensive `?.`, so upstream's newer copies were taken outright — upstream has since fixed the same files. Fork positions kept, each already written down in UPSTREAM.md: the `resolveSelectionHighlight` thunk and `freezeObjectTransform` in the wall systems, the warehouse-scale room test with no upper area bound, the plugin-aware `graph-schema.ts`, the ownership and edit-lease checks on the scene API, `output: 'standalone'`, the warehouse pin, and the vendored articraft and trees workspaces. Four positions were not written down and now are: the room-envelope height caps, which upstream raised from 6 m to 20 m and this fork removed outright because a cap clamps typed input as well as drag; the trees pin, which stays `workspace:*` while the vendored copy exists; the scene-loader's floating navigation; and the site tree's `def.tree` gate, which now lives inside upstream's own `getTreeNodeComponent`. Upstream's empty-graph guard tests failed on arrival for the reason the last merge's log predicted. Their fixture builds an invented `qa:box` kind, which upstream's validator holds to the BaseNode envelope and this fork's refuses outright, so all three saves returned 400 before reaching the guard under test. The fixture now calls `WallNode.parse`, which is the rule that log already drew from `graph-schema.test.ts`. `bun.lock` is committed unchanged. Upstream bumped plugin-bones to 85238a8e, and the lockfile records the sha512 of each GitHub tarball — which only a machine that can reach the real api.github.com can compute. Relock is dispatched on this branch afterwards; until it runs, `--frozen-lockfile` is expected to fail on that hash and nothing else. Also folds in the one item from the audit that this file was already open for: `@pascal-app/plugin-articraft` joins `transpilePackages`. It ships raw TypeScript and `bootstrap.ts` imports it, and it built until now only because bun's symlink layout drops its real path outside `node_modules`. Gates: biome clean, `check-types` clean for every workspace that can resolve its dependencies here, and `bun run test` green across all 16 tasks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
A level's slab and its ceiling sit at opposite ends of the level, so a shaft crossing floors 1-3 cuts slabs 2 and 3 but ceilings 1 and 2. The predicate saw only the level id, so a kind had one answer for both surfaces and was necessarily wrong at one end: slab semantics leave the bottom floor's ceiling sealed across the shaft, ceiling semantics cut a hole in the ceiling above the top stop. The elevator has always had two predicates for exactly this reason; the capability handed plugins a single one. Pass the surface kind — the sync already holds the surface node, so it costs one argument — and the two sides can express the same rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
ovurrsl
force-pushed
the
claude/github-push-turbopack-fix-v0z56s
branch
from
August 24, 2026 14:32
a52e90c to
8194034
Compare
`floorPlaced.collides` is the spatial grid's plan rectangle: XZ only, no height at all. That is right for furniture on a floor and wrong for anything whose usable volume is mostly air — to the grid, a conveyor threading the walkway under a racking run is indistinguishable from one driven through its uprights. Kinds like that must leave `collides` off. Turning it off turned off their move validation entirely. `boxDimensions` is what drives `recomputeValidity`, and it was gated on `collides` alone, so such a kind could be placed through a gate that checks it in three dimensions and then dragged straight into solid steel with nothing consulted again. Placement refused what the drag then allowed. `movable.canMoveTo` is the kind's own answer, run on every pointer move of a drag and composed with the grid's rather than replacing it. Declaring it also turns the green/red box on, because refusing a drop with no visible reason reads as the object simply refusing to move; Alt forces it, exactly as it does for a colliding kind. A kind that declares neither keeps the plain arrow cursor it has always had, which is what the extracted `showsValidityBox` is tested on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
…push-turbopack-fix-v0z56s # Conflicts: # bun.lock
Opening a zone's settings took the whole editor down with React 185 —
maximum update depth exceeded.
The contents selector ran through `useShallow`, which compares array
elements with `Object.is`, and it built fresh `{ label, count }` objects
on every call. Two calls over an identical scene were therefore never
equal, so the snapshot read as changed on every render and the loop never
settled. Every other `useShallow` in this codebase maps ids onto nodes
that already exist; not one constructs an object, and this is why.
The selector now returns strings — one label per node, compared by value —
and the grouping moved out into a `useMemo`. `collectZoneObjectLabels`
sits beside `collectZoneObjectIds` so the property that matters can be
tested without a renderer: two calls over an unchanged scene are
shallow-equal, and a node moving out of the zone still makes them
unequal, so the panel keeps updating. Both fail against the old shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
ovurrsl
marked this pull request as ready for review
August 25, 2026 07:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Üç commit: upstream birleştirmesi, ve üstüne eklenti tarafının beklediği iki çekirdek yeteneği. Bir de Relock'un ürettiği kilit dosyası.
1. Upstream 7f629b8 — 55 commit
240 dosya, +20.477 / −910.
mirror-upstream'in açık tuttuğu PR #5'in içeriği.Ne geldi
Lean-to çatı uzantıları ve otomatik yağmur suyu tahliyesi (pascalorg#651, pascalorg#690), Blender tarzı özel mesh düzenleme (pascalorg#638), senkron 2B görüntüleyici modları (pascalorg#672), homojen çoklu seçimde ortak parametre düzenleme (pascalorg#680), eklenti müfettiş kartı uzantıları (pascalorg#667), boş-graf kayıt koruması, duvar hover/tıklama doğruluğunda bir dizi düzeltme (pascalorg#683, pascalorg#686, pascalorg#687, pascalorg#689, pascalorg#697), ve yükleme penceresinde sahneleri silen otomatik kaydı düzelten yama (pascalorg#682).
Otuz beş çakışmanın beşi gerçek çakışma değildi
integration, upstream'in pascalorg#607, pascalorg#608 ve pascalorg#638'ini cherry-pick olarak taşıyor. Git bunu aynı yolun iki bağımsız eklenmesi olarak görüp add/add bildirdi. Beşinin dördü, alındıkları upstream commit'iyle bayt bayt aynıydı; beşincisi tek bir savunmacı?.ile ayrılıyordu.bunu bir saniyede söylüyor. Dördü için upstream'in yeni hâli doğrudan alındı — upstream o dosyaları o zamandan beri düzeltmiş (pascalorg#686, pascalorg#687), yani "onlarınkini al" burada kesin bir yükseltme.
Korunan fork konumları
UPSTREAM.md'de zaten yazılı olanlar: duvar sistemlerindeki
resolveSelectionHighlightthunk'ı vefreezeObjectTransform, üst alan sınırı olmayan depo ölçeğindeki oda testi, eklenti farkındalıklıgraph-schema.ts, sahne API'sindeki sahiplik ve düzenleme kirası kontrolleri,output: 'standalone', warehouse pin'i, vendored articraft ve trees workspace'leri.Yazılı olmayan dört konum vardı; artık yazılı:
SliderControlsürüklemeyi değil yazılan değeri de kırpıyor, yani sürüklenerek çizilmiş 120 m'lik duvar Length alanına dokunulunca sessizce 20 m oluyordu (fork PR #17, on üç tavan). Upstream'in yapısal değişiklikleri alındı (kolon yüksekliğindeki!managedByLeanTosarmalayıcısı),maxalınmadı@pascal-app/plugin-treespin'iworkspace:*kaldı. Github pin'ine geçmekpackages/plugin-trees(ve yanındaki articraft) ile ne olacağına karar vermeyi gerektiriyor — kendi build'iyle kanıtlanması gereken ayrı bir iş, upstream merge'inin içinde yapılacak şey değil. Bedeli yazıldı: trees upstream güncellemesi almıyorscene-loader.tsxyüzen düğmelerLight preview/All scenesbloğu yine düşürüldü. İkisi de çizimin üstünde duran navigasyondu; Scenes rayı ikincisini editörden çıkmadan cevaplıyor,?disable=postFxhâlâ okunuyordef.treekapısıgetTreeNodeComponent'i alındı (kendi testi onu import ediyor), kapı onun içine taşındı. Upstream koşulsuz geri düşüyor ve bir katın satırı her çocuğunuTreeNode'a veriyor — yani koşulsuz geri düşmeroof-segment,stair-segment, kanal/boru bağlantıları veguideiçin satır çizerdiUpstream'in testi geldiği anda kırmızıydı — ve sebebi geçen merge'in kaydında yazıyordu
Boş-graf koruması testi fixture'ını uydurma bir
qa:boxkind'ıyla kuruyor; gerekçe "yabancı türlü düğüm yalnız BaseNode zarfına tutulur". Upstream'de doğru, burada yanlış:lib/graph-schema.tsbir eklenti kind'ını o eklentinin kendidef.schema'sına, geri kalanıAnyNode'a yönlendiriyor. Uydurma kind reddediliyor, üç kayıt da test edilen korumaya hiç ulaşmadan 400invalid_requestdönüyordu.Fixture artık
WallNode.parse(...)çağırıyor — 2026-08-10 kaydınıngraph-schema.test.ts'ten çıkardığı kuralın ta kendisi, ikinci kez.bun.lockve ilk kırmızı koşuUpstream
plugin-bones'u85238a8e'ye çıkardı. Kilit dosyası her GitHub tarball'ının sha512'sini tutuyor ve onu yalnız gerçekapi.github.com'a ulaşabilen bir makine hesaplayabilir; bu oturumun kapsamı dışındaki depolar için tarball istekleri 403 dönüyor.Kilit olduğu gibi commit'lendi, CI beklendiği gibi kırmızı oldu — tam olarak bir satırda:
Relock bu dal üzerinde dispatch edildi (
f3ab6a39), kilidi gerçek bir runner'da yeniden üretti ve dala geri push etti. Sonraki commit onun üstüne bindi.Bir de denetim listesinden bir madde
@pascal-app/plugin-articraftartıktranspilePackages'ta. Ham TypeScript yayınlıyor ("main": "./src/index.ts"),lib/bootstrap.tsonu import ediyor, ve listede hiç yoktu. Bugüne kadar yalnızca bun'ın symlink düzeni gerçek yolununode_modulesdışına düşürdüğü ve Next onu yine de derlediği için çalışıyordu — linker davranışı değişse uyarısız kırılırdı.2. Dikey açıklık yüzeye göre cevap versin
Bir katın döşemesi ile tavanı katın iki ucunda durur. 1–3 katları arasında çalışan bir kuyu, döşemelerden 2 ve 3'ü, tavanlardan 1 ve 2'yi keser — aynı küme değil.
VerticalOpeningConfig.servesLevelyalnız kat kimliğini görüyordu, yani bir kind iki yüzeye tek cevap veriyor ve bir uçta zorunlu olarak yanılıyordu: döşeme mantığıyla en alt katın tavanı kuyunun önünde kapalı kalır, tavan mantığıyla en üst durağın üstündeki tavanda olmaması gereken bir delik açılır.Host asansörünün kendi kodunda bu yüzden hep iki ayrı yüklem vardı (
shouldApplyElevatorToSlab/shouldApplyElevatorToCeiling); eklentilere açtığımız yetenek tek yüklem veriyordu. Yüzey türü de geçiriliyor — senkronizasyon zaten yüzey düğümünü elinde tuttuğu için maliyeti tek argüman (surface.type).Yeni test yüzeye göre ayrım yapan bir kind kaydediyor, aynı katta hem döşeme hem tavan bulunan bir sahne kuruyor, döşemenin kesildiğini ve tavanın kesilmediğini doğruluyor. Argümanı geçirmeden koşunca gerçekten düşüyor:
3. Bir kind kendi sürüklenmesini de denetleyebilsin
floorPlaced.collidesuzamsal ızgaranın plan dikdörtgenidir: yalnız XZ, hiç yükseklik yok. Zemindeki mobilya için doğru, kullanılabilir hacmi çoğunlukla hava olan her şey için yanlış — ızgaraya göre tünelli bir gözün altındaki yürüme yolundan geçen bir konveyör ile dikmelerinin içinden sürülen bir konveyör aynı şeydir. Böyle kind'larcollides'ı kapalı tutmak zorunda.Kapalı tutmak sürükleme denetimini tümüyle kapatıyordu.
recomputeValidity'yi süren şeyboxDimensionsve o yalnızcollides'a bağlıydı; yani böyle bir kind üç boyutta denetleyen bir kapıdan geçip yerleşiyor, sonra hiçbir şey sorulmadan doğrudan çeliğin içine sürüklenebiliyordu. Yerleştirmenin reddettiğini sürükleme kabul ediyordu.movable.canMoveTokind'ın kendi cevabı: sürüklemenin her fare hareketinde koşuyor ve ızgaranınkinin yerine geçmiyor, onunla birleşiyor. Bildirmek yeşil/kırmızı kutuyu da açıyor — görünür bir sebep vermeden bir bırakmayı reddetmek, nesnenin öylece kımıldamayı reddetmesi gibi okunur; Alt zorluyor, tam olarak çarpışan bir kind'da olduğu gibi. Hiçbirini bildirmeyen kind eskiden beri sahip olduğu düz ok imlecini koruyor — çıkarılanshowsValidityBoxtam olarak bunun üstünden test ediliyor.Eklenti tarafı ikisini de kullanıyor: ovurrsl/plugin-warehouse#34 — kat delikleri, sürükleme kapısı ve asma katın kendi çarpışması. O PR yeşil.
Kapılar
Merge sonrası, yayınlamadan önce
UPSTREAM.md'nin "Before you publish: keep a way back" bölümü geçerli: canlı deploy'un build edildiği sha'yı (son başarılıDeploy bundlekoşusununhead_sha'sı —integration'ın ucu değil) bir rollback dalı olarak itin.