Add pixi-react UI foundation with a shared renderer, port PurseView - #34
Open
billsonnn wants to merge 183 commits into
Open
Add pixi-react UI foundation with a shared renderer, port PurseView#34billsonnn wants to merge 183 commits into
billsonnn wants to merge 183 commits into
Conversation
Invert the render bootstrap so the room and the Pixi-rendered UI share a single Application/renderer/GPU context (no duplicate texture uploads): PixiApplicationRoot now owns the one renderer, adopted via a new SetRenderer() in nitro-renderer, and mounts the room's existing stage as its bottom layer. RoomCanvas and Nitro's bootstrap sequencing are updated accordingly. Add a theme-pixi package mirroring the DOM theme system's public shape (Box as the flex/positioning primitive via @pixi/layout, Border/Button with native Pixi tinting and nine-slice sprites, icon/text-style/ interaction-state hooks), reusing the existing variant-cascade system verbatim. Port PurseView to PurseViewPixi as a working proof of concept. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
@pixi/layout only positions/sizes an object if `.layout` is explicitly
set on it (confirmed by reading Layout.getRoot()/LayoutSystem.mjs in
node_modules): an object with no `.layout` is invisible to the yoga
tree and stays at Pixi's raw default position. NitroIcon,
NitroCurrencyIcon, and Button's inline text never set `layout` at all,
so icons and text ignored gap/justifyContent/sizing entirely. Both
components now default `layout` to `{}` so leaf content always joins
the tree (still overridable via a new `layout` prop).
Also, app.stage - the root of the yoga tree for all Pixi UI - never
had its own `.layout`, so a top-level `position: 'absolute'` child
(e.g. PurseViewPixi's outer Border) had no sized, positioned ancestor
to resolve `top`/`right`/etc against and landed at (0, 0) regardless.
PixiApplicationRoot now gives app.stage a `.layout` matching the
renderer's screen size (position: 'relative', so it's a valid
containing block), kept in sync on the renderer's 'resize' event.
Verified with a headless-browser pixel readback of the actual canvas
(toDataURL, since preserveDrawingBuffer is normally false): the Purse
widget now renders in the correct screen position with icons and text
correctly placed by the flex layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Frame + its supporting pieces (Header, ContentArea, Scaler, CloseButton) port theme/Frame.tsx and its siblings for variants '0' and '3' (the two actually used across the DOM views: FriendList/dialogs use '0', Catalog/ Inventory use '3'). Reuses the DOM package's variant-cascade system verbatim, same as Border/Button. Dragging and resizing are Pixi ports of hooks/ui/useFrameDrag.ts and useFrameResize.ts: - Pixi's FederatedPointerEvent has no setPointerCapture (confirmed absent from pixi.js's own types), so both hooks drive their gesture off window-level pointermove/pointerup listeners - the same technique the DOM drag hook already uses, just applied to resize too. - The drag offset is applied as the frame's own `.x`/`.y`: @pixi/layout ADDS the yoga-computed position on top of a container's existing `.position` (see updateLocalTransformWithLayout in node_modules/@pixi/layout), the same layering CSS `transform: translate()` gives the DOM version, so no fighting with the layout system for position during a drag. - Frame's width/height precedence now matches the DOM version: an active resize (stored/dragged size) overrides the caller's own layout, the same way the DOM Frame's resize-driven inline style out-specifies its className defaults - caught by testing, the first pass had this backwards. Also fixes a real gap found while building this: nine-slice frame/header art and the closebutton/scaler sprites needed slicing a sub-region out of a shared spritesheet texture (CSS background-position in the DOM version) - added utils/useSpriteFrameTexture.ts for that, and registered TilingSprite (the header's repeating background) in pixiElements.ts. Verified end-to-end with two live Frame instances (variants 0 and 3) in a headless-browser pixel readback: correct position, size, drag-ready z-order/header, close button, and content placement, with zero console errors. Not wired into any real view yet (out of scope for this pass, matching the PurseView-proof-of-concept precedent) - Frame is ready for whichever windowed view migrates next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…not just Box
Reported crash: "Text instances are not yet supported" when a Frame's raw
string children (e.g. `<Frame>some text</Frame>`) reached Pixi's reconciler,
since - unlike a DOM <div> - pixiContainer has no implicit text-node concept.
Box already special-cased its own JSX-authored children, but that only
covers `<Box>{string}</Box>` directly. ContentArea and Border both forward
an arbitrary caller-supplied `children` through VariantCascadeProvider (a
passthrough Fragment/Context.Provider with no host node of its own) before
it lands inside their own Box - so a raw string never appears as a
JSX-authored child of that Box; it only surfaces once React fully resolves
the passthrough wrapper at render time, past the point Box's own
Children.map can see it.
Extracted the wrapping helper to theme-pixi/utils/wrapTextChildren.tsx and
apply it directly in ContentArea and Border, at the point where each still
owns the caller's children, matching the exact Frame -> ContentArea ->
raw-string stack trace from the report.
…der parity
Batch 1+2a of the theme parity pass: cross-cutting rendering primitives that
every remaining component builds on, plus Border.tsx expanded from 2 to all
19 DOM variants as the reference implementation for the new patterns.
- utils/Layer.tsx: NineSliceLayer/SpriteLayer/TileLayer wrap the existing
fill-absolute + eventMode="none" background-layer idiom already used ad
hoc in Frame/Header; CompositeLayer renders the multi-piece DOM variants
(Border 101/102/103/106/107, and later Button/ContainerButton/Frame) as
positioned pixiSprites via the same absolute+inset stretch mechanism
@pixi/layout already uses everywhere else in this package; BlendOverlay
reproduces Border's `blend` white-wash tint as a live masked Graphics
overlay (same masked-Sprite technique as theme/utils/pixiTint.ts's
tintImage(), just without its canvas-bake round trip, which buys nothing
in Pixi).
- utils/textStyles.ts: expanded from 2 to all 18 real text-style-* keys in
theme/utilities.css (font family/size, plus the 3 utilities that bake in
their own fill color). The two Frame-referenced classnames that don't
actually exist in utilities.css (text-style-id-frame-title,
text-style-il-frame-modal-title - dead Tailwind utilities in the DOM
source) are intentionally not ported; those Frame variants get no special
text style, matching what DOM actually (fails to) render.
- Border.tsx: all 19 variants. Plain nine-slice variants (0-10, 100, 104,
105, 108, 200) use NineSliceLayer; the 5 composite variants (101, 102,
103, 106, 107) use CompositeLayer with piece geometry transcribed from
the DOM's background-position/background-size values, including two
cross-piece asset-reuse quirks (101 and 107 both reuse their center-left
texture for center-right) preserved as-is to match shipped art. 104/105
get their overlay via NineSliceLayer/CompositeLayer; `blend` support
added via BlendOverlay. Tint colors ('9', '108') match DOM exactly.
Variant '100' is the one DOM variant whose border-image-slice omits
`fill` (CSS leaves the center transparent); Pixi's NineSliceSprite has
no equivalent, so this port always fills the center - noted in a comment
rather than silently diverging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…ded components Batch 2 of the theme parity pass: every component previously ported with only the 1-2 variants the PurseView/Frame proof-of-concept used now covers its complete theme/ variant table, including tint colors, overlay/shine layers, and (for Frame/Button/ContainerButton-style composites) the multi-piece background composites, using the Layer.tsx/textStyles.ts primitives from the previous commit. - Border: 2 -> 19 variants (composite variants 101/102/103/106/107 via CompositeLayer, overlay variants 104/105, blend support, full tint table). - Button: 1 -> 13 variants (per-state nine-slice geometry since 100/101 change slice width between default and hover/active; 11-piece composite overlay for 100/101 with its pressed-state asset+spacer swap, 2-piece curve overlay for 102/103; per-variant text style/color; '1's pressed state correctly reuses `border-3-default-src`, a real DOM cross-asset quirk, not a bug). - CloseButton: 2 -> 7 variants, including '1'/'2's static (no real hover, active===default) art and '100's separate fixed-size texture. - Scaler: 2 -> 6 variants + the per-variant offset-positioning table (only '0'/'3' actually position bottom-right in DOM - '1'/'2'/'4' define no offset class at all, preserved as-is) + shine overlay for 0/1/2. - Header: 2 -> 8 variants (tile/stretch/nine-slice backgrounds, shine overlay for 0/1/2, the caption/close-button tint-color background chip, per-variant text style/color/font-aa drop shadow, and the real margin- vs-padding inconsistency between variants 0/2 and 1 preserved via Yoga margin vs. padding rather than flattened to one behavior). - Frame: 2 -> 9 variants, the universal drop-shadow (all DOM variants get `drop-shadow-[2.83px_2.83px_4px_rgba(0,0,0,0.349)]`) via a Pixi DropShadowFilter, shine overlays, and variant '100' reusing Border's own `border-101-*` composite asset set wholesale (a real DOM asset-reuse quirk). - ContentArea: fixed a real bug (not just an unported variant) - variant was accepted as a prop and cascade-resolved but had zero effect on the render; now applies '0's bottom padding and '3's `position: relative`. - NitroIcon/useIconTexture: 4 -> 264 icon keys (184 discrete PNGs from icons.css, 80 spritesheet frames from habbo-icons.css), full parity with both DOM icon registries. - NitroCurrencyIcon: added the DOM `Image.tsx`-equivalent loading/error fallback (a `loading.icon.url` texture shown whenever the real currency texture hasn't resolved, matching DOM's "stays visible forever on error" behavior since Pixi has no distinct loading-vs-errored texture state). - Added `pixi-filters` (already a transitive/hoisted dependency via nitro-renderer) as a direct nitro-react dependency for DropShadowFilter. Found and fixed two real bugs during verification (headless Playwright, multiple simultaneous Frame instances each containing Button+Border): 1. FRAME_3_SHINE's nine-slice args were passed in the wrong order (leftWidth/topHeight swapped vs. the base layer), corrupting variants 3/4/7's shine overlay geometry. 2. The new DropShadowFilter defaulted to resolution 1 while the app renders at `GetPixelRatio()` - the mismatch caused a stale-texture ghosting artifact (a fragment of one Frame's content bleeding into another's) whenever 2+ filtered Frames rendered in the same tick, only reproducible with multiple simultaneous Frames. Fixed by passing the app's own resolution to the filter explicitly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…eckBox, RadioButton, Bubble family Batch 3 of the theme parity pass: the remaining "simple" theme/ primitives that weren't part of the original PurseView/Frame proof-of-concept, each following the variant-table + Layer.tsx recipe established in the previous two commits. - ButtonThick: 6 variants (0-2 stretch sprites, 3-6 nine-slice), per- variant text style/color, no overlay (DOM's overlay config exists but every entry is empty). - ContainerButton: 14 variants. Extensive real cross-component asset reuse confirmed directly from the DOM source (not guessed): 0/1/2 reuse Button's own textures, 3/6 reuse ButtonThick's, 100-103 reuse Button's 100-103 textures AND overlay composites verbatim - extracted the shared 11-piece/2-piece composite-overlay data Button and ContainerButton both need into utils/buttonOverlayPieces.ts (Button.tsx now imports from there too, replacing its former local copy). Preserves DOM's missing `not-aria-disabled:` cursor guard (ContainerButton always shows a pointer cursor, unlike Button/ButtonThick) and variant '1's `border-3-default-src` pressed-state quirk (same as Button's own). - ButtonGroupLeft/Center/Right: 4 variants each, sharing one `createButtonGroupComponent` factory (utils/buttonGroupFactory.tsx) since the three DOM components are otherwise byte-identical apart from asset-key prefix and slice geometry. `aria-selected`/`active:` collapse into one Pixi `selected` state since DOM swaps to the same art for both. - CheckBox/RadioButton: sprite-sheet toggle components, `selected` fully caller-controlled (matching DOM's `aria-selected`-driven, non- self-toggling design). Preserves CheckBox's real DOM quirk where pressing a checked box reverts to the unchecked frame while held down, and both components' variant '2' reusing variant '0's exact sprite positions. - Bubble/BubblePointer/FramePointerDown: chat-bubble family. Bubble's pointer-direction layout (up/down/left/right) ported via `flexDirection`/`flexDirection-reverse`; BubblePointer's 4 direction configs each get their own sprite-sheet frame + per-side Yoga margin matching DOM's small negative-margin nudge into the bubble body. FramePointerDown is DOM's simplest component (one variant, no states). Verified via headless Playwright: all new components render correctly inside Frames (selected states, nine-slice chrome, sprite frames, text) with no console/page errors and no ghosting artifacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t/Tooltip Batches 4-6 of the theme parity pass. Tab family (TabContext, TabContent, TabButton, TabContainerButton): a shared utils/tabButtonChrome.tsx holds the border-image variant table used by both TabButton and TabContainerButton (identical chrome, TabButton adds padding/sizing/text on top of it - mirrors the buttonGroupFactory.tsx pattern from the previous commit). TabContent's variant '3' has an asymmetric nine-slice (only the top edge sliced) plus a negative top margin tucking it under the tab bar, reproduced via Yoga's negative-margin support. `aria-selected:`/`active:` collapse into one `selected` prop throughout, consistent with every other selection-driven component ported so far. VARIANT_CASCADE_CONFIG's existing `tabContext` -> `tabButton` cascade (reused verbatim from #base/theme) means a TabButton nested under a shiny TabContext auto-renders as the shiny variant with no explicit prop. Accordion family (Accordion, AccordionItem, AccordionContext, AccordionTrigger, AccordionContent): AccordionContext is pure React Context with zero DOM dependency, so it's re-exported verbatim from #base/theme rather than forked - and Accordion.tsx reuses that same Context *object* directly (not a re-implementation), so a Pixi Accordion.Provider and the shared useAccordion/useAccordionItem hooks stay correctly wired together. AccordionContent has zero open/close animation in DOM (hard conditional mount/unmount, no transition, no Motion usage anywhere in it or its call sites) - the instant show/hide here is exact parity, not a cut corner. AccordionTrigger's `alwaysOpen` case maps to `eventMode="none"` in Pixi, which is strictly more correct than DOM's still-technically-wired-but-inert click handler (it also lets pointer events reach whatever's beneath the trigger, which DOM's div can't do). Dropmenu/DropmenuItem/Droplist/DroplistItem/Tooltip: static-skinning parity only. Confirmed via full-codebase research earlier in this pass that DOM ships zero open/close/positioning/portal logic for any of these five - Dropmenu is only ever rendered as a bare closed box with no items, Droplist/DroplistItem/Tooltip/DropmenuItem have no call sites at all - so this port matches that reality (variant/tint/overlay-arrow art only) and does not add new floating-menu behavior DOM never had. Tooltip's DOM class string (`text-[#000000] text-style-u-tool-tip`) actually renders WHITE text in the browser, since text-style-u-tool-tip's own baked-in `text-white` is the later, winning declaration in Tailwind's cascade - reproduced here via the text style's own color rather than the literal (losing) black hex, matching real DOM output over the misleading literal. Verified via headless Playwright: tab switching, accordion open/close, dropdown arrow overlays, and selection states all render correctly inside Frames with no console/page errors and no ghosting artifacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports theme/'s scroll system to theme-pixi/: useScrollController (Pixi
equivalent of useScrollbarController, since Pixi has no native scroll to
drive/be driven by), ScrollViewport (masked clipping window), useHoldToRepeat,
useRowVirtualizer (a small from-scratch row virtualizer replacing
@tanstack/react-virtual, which is inherently shaped around a DOM scroll
element Pixi doesn't have), the full ScrollbarVertical/Horizontal chrome
family, ScrollArea, and InfiniteGrid.
Also fixes a real, previously-shipped rendering bug found while building this:
a plain pixiSprite/pixiGraphics doesn't stretch to fill its parent via
`layout={{position:'absolute', top:0, left:0, right:0, bottom:0}}` alone in
this @pixi/layout version - it needs explicit width/height:'100%' too, since
NineSliceSprite/TilingSprite are the only leaf types that apply their computed
size directly. Fixed in Layer.tsx's shared FILL_LAYOUT (used by SpriteLayer/
BlendOverlay) and Header.tsx's TintChip.
This completes the theme-pixi package's full variant/color/overlay parity
with theme/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t' into claude/pixi-react-ui-theme-uj63lt
Every component repeated the same three blocks of logic verbatim: - Variant resolution (useCascadedVariant + resolvedVariant + ownCascade lookup) - Interaction-state -> art resolution (resolveLayerState/resolveTabChromeLayer, identical in Button.tsx, ContainerButton.tsx, buttonGroupFactory.tsx and tabButtonChrome.tsx) - Background-layer kind switching (nineSlice/composite/tile/stretch), duplicated character-for-character between Border.tsx and Frame.tsx and reimplemented separately in Header.tsx Extracted three shared primitives and adopted them everywhere they apply: useResolvedVariant (utils/useResolvedVariant.ts), resolveByState + NineSliceLayerState/nineSliceLayerState (utils/useInteractionState.ts), and BackgroundLayer (utils/Layer.tsx). Applied to Border, Frame, Header, Button, ContainerButton, buttonGroupFactory (used by ButtonGroupLeft/Center/Right), tabButtonChrome (used by TabButton/TabContainerButton) - no behavior change, verified via a visual regression pass showing identical rendered output before/after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Mechanical follow-up to the shared theme-pixi primitives consolidation: replaces the repeated cascadedVariant/resolvedVariant/ownCascade block with useResolvedVariant() in the 12 scrollbar-family components (ScrollbarVertical/ Horizontal, all slider bar/track/button sub-parts, ScrollableItemList/GridVertical). No behavior change - eslint/tsc verified clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t' into claude/pixi-react-ui-theme-uj63lt
Mechanical follow-up completing the shared-primitives consolidation: replaces the repeated cascadedVariant/resolvedVariant/ownCascade block with useResolvedVariant() in Bubble, BubblePointer, ButtonThick, CheckBox, CloseButton, ContentArea, Droplist, DroplistItem, Dropmenu, DropmenuItem, FramePointerDown, RadioButton, Scaler, TabContent, TabContext, Tooltip. Also opportunistically replaced three local per-state art resolvers (DroplistItem, ButtonThick, CloseButton) with the shared resolveByState utility where their state tables were an exact shape match. No behavior change - eslint/tsc verified clean, visual regression pass shows identical rendered output before/after across all touched components. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
First batch of the views/ -> views-pixi/ migration (see
/root/.claude/plans/pixi-views-migration.md for the full plan): ports
ActivityPointsView, AvatarEditorView and MessengerView, following the
Wrapper/Component/ViewPixi pattern already established by the Purse/Catalog
migrations. AvatarEditorView and MessengerView are themselves DOM stubs
("test"/"chats" placeholder content) - ported at the same stub fidelity.
Adds a shared ColorLayer primitive to theme-pixi/utils/Layer.tsx (a flat
solid-color fill, matching a DOM `bg-[#hex]` div) and switches Header.tsx's
local TintChip over to it, since views will need the same solid-fill pattern
constantly. Exposes utils/Layer.tsx from the theme-pixi/ barrel now that
views (not just theme-pixi components) need to consume it directly.
Splits MessengerComponent out of FriendListWrapper so it can move into
Nitro.tsx's Pixi tree independently - FriendListComponent still renders DOM
views and can't move until FriendList's own migration.
NotificationCenterView (theme/'s own source always returns null) and
HotelView (empty div, wrapped in DOM-only motion.js transition machinery
outside the view itself) have nothing to migrate - dropped rather than
ported as no-ops, noted in MainView.tsx.
Verified via eslint/tsc (105-error pre-existing baseline, zero new) and a
Playwright visual check of all three views rendering correctly with no
console errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Adds useAvatarImageTexture (wraps AvatarRenderManager's already-decoded cropped avatar image in a Pixi Texture.from(), reusing the same engine call components/AvatarImage.tsx makes rather than round-tripping through a CSS background-image), a shared ToolbarMenu component behind ToolbarMeMenuPixi/ToolbarProgressionMenuPixi (structurally identical DOM sources), and ToolbarViewPixi itself (collapse toggles, me/progression popups, circular avatar icon masked via Pixi). Wires ToolbarViewPixi into Nitro.tsx's PixiApplicationRoot in place of the DOM ToolbarView. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
InventoryFurniViewPixi reuses theme-pixi's row-virtualizer/scroll- controller primitives (the same ones InfiniteGrid is built on) directly with a fixed 4-column layout, matching DOM's own hand-rolled react-virtual usage rather than InfiniteGrid's auto-column behavior. InventoryPetsView/BotsView/BadgesView (byte-identical DOM stubs) share one InventoryStubTabPixi. Wires InventoryComponent into Nitro.tsx's PixiApplicationRoot in place of the DOM InventoryView. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Adds two new shared theme-pixi primitives this migration needed for real (not stub) DOM behavior: TextInput (a faked editable text field - Pixi has no native editable text object - backing every real search/message box hit so far) and useOutsideClick (geometric hit-test port of hooks/ui/useOutsideClick.ts, used by the friend-row relationship dropdown). Extends AccordionTrigger with optional onPointerOver/onPointerOut passthrough for FriendListTab's hover-driven footer tooltip text. Ports the full FriendList tree: root view, the shared Tab wrapper (DOM's hard-edged two-color trigger gradient reproduced as two ColorLayer halves), Friends/Requests/Search tabs, group/item leaves, footers, and the two dialogs. Wires FriendListComponent to render the Pixi tree and moves FriendListWrapper's mount point into Nitro.tsx's PixiApplicationRoot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the Navigator window: root frame (collapsible left pane, dynamic frame width, top-level context tabs), quick links pane, search bar, category/room-entry results (rows and tiles modes, with the SWF's alternating-row-color and user-count-color math reused verbatim from NavigatorRoomEntryUtils.ts, which has no DOM dependency), and the create/random/promote room action buttons. Drops the DOM version's getBoundingClientRect-driven window-position telemetry sync (Frame doesn't forward a ref; not worth widening its contract for a background-only feature). Extends Dropmenu/DropmenuItem with an optional onPress - unlike every other confirmed-dead Dropmenu call site found so far, NavigatorSearchView uses it as a real interactive filter-type picker. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the full catalog tree: root frame with tabs, header, search, navigation tree, item grid, product preview, purchase widgets, and the purchase confirmation dialog. Adds two new engine-integration hooks reusing existing DOM logic verbatim (both are pure engine/state logic with the DOM-specific I/O swapped out): - useFurnitureImageTexturePixi mirrors useAvatarImageTexture's own approach for components/FurnitureImage.tsx's GetRoomEngine() call. - useRoomPreviewerPixi mirrors hooks/room/useRoomPreviewer.tsx, but mounts the temp room's own `room.canvas.master` (confirmed to be a plain Pixi Container) directly into the UI's Pixi tree instead of DOM's extract-to-2D-canvas-every-tick round trip - a genuine simplification once room and UI share one renderer, not just a port. Adds a shared Image component generalizing the loading-placeholder pattern NitroCurrencyIcon.tsx already established for its own image src. Extends the AccordionTrigger circular-import port pattern to CatalogNavigationSetView/CatalogNavigationSetItemView's own recursion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the object-infostand and object-menu view families to Pixi equivalents (InfostandBot/Pet/Furni/User, InfoBubbleName/Avatar/ OwnAvatar), plus RoomObjectMenuBubblePixi - a Pixi port of the imperative screen-position-tracking bubble container that mutates a Container ref's x/y/alpha/visible on every RoomRenderedEvent tick instead of DOM style properties - and RoomWidgetsPixi, which replaces the DOM createPortal root with a normal Pixi child carrying its own RoomEngineEvent-driven isReady state. The four logic-only wrapper components (RoomObjectInfostandWidget, InfostandFurni, RoomObjectMenuNameBubble, RoomObjectMenuWidget) are edited in place to point at the new Pixi views, and RoomWidgetsPixi is wired into Nitro.tsx in place of RoomContainer's DOM RoomWidgets portal. This completes the "convert all views to Pixi" migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Every themed background/overlay layer (NineSliceLayer, SpriteLayer, ColorLayer, CompositePieceSprite, BlendOverlay) sets eventMode="none" so its art doesn't shadow its own container's press handlers, and nothing ever gave the interactive container itself an explicit hitArea. Per Pixi's own EventBoundary (hitPruneFn/hitTestFn), a static/dynamic Container with no hitArea has no shape of its own to hit-test against, so its clickable region silently collapsed to whatever passive/interactive children happened to sit under the pointer - in practice just a text label, even though the button's rendered art fills the whole box. Box now attaches a self-tracking fallback hitArea on mount (skipped if the caller already set one) that re-reads the container's own yoga-computed size on every hit test, so it stays correct as content/ padding resize the box without an effect keeping a baked Rectangle in sync. Since every pressable primitive (Button, CloseButton, ContainerButton, Header, Frame, tabs, accordion triggers, dropmenu items, ...) is built on Box, this fixes the whole family in one place. Verified interactively: a corner click away from a button's centered text now fires onPress, same as clicking the text itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Replaces every <pixiText style={getPixiTextStyle(key, overrides)}>
call site across theme-pixi and views-pixi with <Text textStyle={key}
textOptions={overrides}>, matching the pattern Button.tsx already
established. Left untouched: Text.tsx itself (the implementation),
utils/wrapTextChildren.tsx (the unstyled raw-string safety net for
Box's string children - Text's textStyle prop is required, and this
is deliberately unstyled), and the one fallback branch each in
CheckBox.tsx/RadioButton.tsx that renders unstyled pixiText when a
variant carries no textStyleKey at all (0/1/2, which have no label in
DOM either).
Every file's getPixiTextStyle import is removed once its only call
site becomes redundant (verified 1:1 per file first). Verified via
tsc --build --force (stable, no new errors) and a Playwright
screenshot comparison against the pre-conversion room-widgets/Button
render, pixel-identical.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
added 30 commits
August 28, 2026 20:49
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.
Invert the render bootstrap so the room and the Pixi-rendered UI share
a single Application/renderer/GPU context (no duplicate texture
uploads): PixiApplicationRoot now owns the one renderer, adopted via a
new SetRenderer() in nitro-renderer, and mounts the room's existing
stage as its bottom layer. RoomCanvas and Nitro's bootstrap sequencing
are updated accordingly.
Add a theme-pixi package mirroring the DOM theme system's public shape
(Box as the flex/positioning primitive via @pixi/layout, Border/Button
with native Pixi tinting and nine-slice sprites, icon/text-style/
interaction-state hooks), reusing the existing variant-cascade system
verbatim. Port PurseView to PurseViewPixi as a working proof of concept.
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa