Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | 'use client' // /create/abacus — "Your Abacus": a full-bleed, Figma-style design studio for // making your on-screen abacus a real, camera-readable object (Gitea epic #5, // full-bleed CP1a). One shared design (columns + colors, grounded in the player's // AbacusDisplayConfig identity) has two first-class OUTPUTS, chosen as an axis in // the left rail rather than an up-front fork: // • paper markers — stick-on ArUco corners for an abacus you already own, and // • 3D print — a printed frame with the markers baked in. // Both converge on the same read-back: 📷 My Abacus finds the four markers and // rectifies the user's N-column abacus from a camera. The immersive studio IS the // page; the express (paper) lane never loads three.js. Absorbs the former // /create/vision-markers and /toys/abacus-studio routes. import dynamic from 'next/dynamic' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { AbacusMarkerSheet } from '@/components/create/abacus/AbacusMarkerSheet' import { AbacusStudioProvider, useAbacusStudio, } from '@/components/create/abacus/AbacusStudioContext' import { intentOf } from '@/components/create/abacus/abacus-fabrication' import { DesignInspectorRail } from '@/components/create/abacus/DesignInspectorRail' import { FabricationErrorBoundary } from '@/components/create/abacus/FabricationErrorBoundary' import { FabricationRail } from '@/components/create/abacus/FabricationRail' import { FabricationSwitch } from '@/components/create/abacus/FabricationSwitch' import { studioHref } from '@/components/create/abacus/studio-url' import { PageWithNav } from '@/components/PageWithNav' import { StudioShell } from '@/components/studio/StudioShell' import { STUDIO } from '@/components/studio/theme' import { useAbacusDesignSnapshot } from '@/hooks/useAbacusDesignSnapshot' import { usePlayerAbacusIdentity } from '@/hooks/usePlayerAbacusIdentity' import { css } from '../../../../styled-system/css' // three.js + OpenSCAD-WASM is heavy; only load it on the 3D-print target. The // paper lane fits the far larger no-3D-printer audience and needs none of it. const AbacusStudioViewer = dynamic( () => import('@/components/create/abacus/AbacusStudioViewer').then((m) => m.AbacusStudioViewer), { ssr: false, loading: () => ( <div data-element="abacus-viewer-loading" className={css({ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 'sm', })} style={{ color: STUDIO.color.text2 }} > Loading 3D preview… </div> ), } ) export default function CreateAbacusPage() { // Whose abacus is being made real: ?player=<id> selects a student (the studio // manifests their "my abacus"), absent = the signed-in user's own config. The // URL carries the selection so links shared across a family/classroom open on // the same student. A failed player fetch (revoked share, bad link) degrades to // the user's own abacus with a notice — the studio never dead-ends. const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const selectedPlayerId = searchParams.get('player') const playerIdentity = usePlayerAbacusIdentity(selectedPlayerId) const playerUnavailable = selectedPlayerId !== null && playerIdentity.isError // ?design=<id> (Gitea #22): a persisted snapshot to restore — the deep link // THH's "Edit in Abacus Studio ↗" carries. A failed read (not yours, gone) // degrades exactly like a failed player: the studio opens normally with a // notice, never a dead-end. The provider only ever sees a loadable id. const designId = searchParams.get('design') const designSnapshot = useAbacusDesignSnapshot(designId) const designUnavailable = designId !== null && designSnapshot.isError const selectPlayer = (playerId: string | null) => { router.replace(studioHref(pathname, { playerId, designId }), { scroll: false }) } // The link chip saved the design (Gitea #25): mirror the minted id into the // URL so the address bar matches the copied link and a reload reopens the // save. Safe against self-stomp — the controller pre-seeds its hydration // guard and the query cache before this ever runs. const handleDesignSaved = (id: string) => { if (id === designId) return router.replace(studioHref(pathname, { playerId: selectedPlayerId, designId: id }), { scroll: false, }) } return ( <PageWithNav navTitle="Your Abacus" navEmoji="🧮"> <div data-component="create-abacus" className={css({ // full-bleed: fill the viewport below the fixed nav (100dvh so the // mobile URL bar doesn't clip the studio). height: '100dvh', pt: 'var(--app-nav-height)', })} style={{ background: STUDIO.color.bg }} > <AbacusStudioProvider playerId={playerUnavailable ? null : selectedPlayerId} designId={designUnavailable ? null : designId} > <StudioBody selectedPlayerId={selectedPlayerId} onSelectPlayer={selectPlayer} playerUnavailable={playerUnavailable} designUnavailable={designUnavailable} onDesignSaved={handleDesignSaved} /> </AbacusStudioProvider> </div> </PageWithNav> ) } interface StudioBodyProps { selectedPlayerId: string | null onSelectPlayer: (playerId: string | null) => void playerUnavailable: boolean designUnavailable: boolean onDesignSaved: (designId: string) => void } // Inside the provider, so the shell's center/right can switch on the shared // design's fabrication axis. The headline chooser (paper ↔ 3D print) rides in the // shell's persistent top toolbar so it reads on every breakpoint; the shell swaps // the center output (and the FDM-only fabrication rail) accordingly. Only the // 3D-print target mounts the heavy three.js viewer + fabrication rail. function StudioBody({ selectedPlayerId, onSelectPlayer, playerUnavailable, designUnavailable, onDesignSaved, }: StudioBodyProps) { const { fabrication } = useAbacusStudio() const isFdm = fabrication.kind === 'fdm' return ( <StudioShell toolbar={<FabricationSwitch />} left={ <DesignInspectorRail selectedPlayerId={selectedPlayerId} onSelectPlayer={onSelectPlayer} playerUnavailable={playerUnavailable} designUnavailable={designUnavailable} onDesignSaved={onDesignSaved} /> } center={ isFdm ? ( <FabricationErrorBoundary label="3D preview"> <AbacusStudioViewer /> </FabricationErrorBoundary> ) : ( <PaperMarkerCenter /> ) } right={ isFdm ? ( <FabricationErrorBoundary label="print panel"> <FabricationRail /> </FabricationErrorBoundary> ) : null } /> ) } // The paper target's center: the stick-on marker sheet, driven wholly by the // shared design through the fabrication-neutral seam (intentOf) — column count // AND marker size — so the paper output is provably the same design the 3D // print renders, and cannot drift from it. Size edits route back into the // design (set('marker_mm', …)), reshaping the FDM frame recess too. function PaperMarkerCenter() { const { design, set } = useAbacusStudio() const intent = intentOf(design) return ( <div data-element="abacus-tool-markers" className={css({ h: 'full', overflow: 'auto', bg: 'bg.canvas', px: { base: 4, md: 6 }, py: { base: 4, md: 6 }, })} > <AbacusMarkerSheet columnCount={intent.columns} markerSizeMm={intent.markers.sizeMm} onMarkerSizeChange={(mm) => set('marker_mm', mm)} /> </div> ) } |