@fornerds/3d-solution-core
Generic 3D core, ESM and TypeScript declarations. No showroom data or models.
Development
Use .nvmrc, then npm install, npm run format, npm run check, npm pack.
.npmrc reads NODE_AUTH_TOKEN from the environment; export it (any value works offline) before running npm.
Commit the generated package-lock.json before the first CI run. Subsequent installs use npm ci.
The package peers match stack.json exactly. React and Three are provided by consumers.
Core changes need an ADR; see docs/adr/ for the existing records.
Public API
SpaceCanvas: camera/render defaults, optional render metrics (previous frame draw calls/triangles, mean frame time),webglFallbackfor the no-context message.WalkController: focus Canvas, WASD/arrows, pointer drag, resetKey, pause, XZ collision. It resets onresetKeyor on a changed spawn value, so an inline spawn literal is safe to pass.speedis metres per second (default 1.7) andturnSpeedradians per second (default 1.6); both take effect on the next frame, so a slider can drive them without remounting or resetting the camera.collision={false}walks through obstacles.navigation.boundsstill holds, so the visitor stays inside the space andresetKeyalways recovers. Zoom is on by default: wheel, two-finger pinch and+/-change the lens, not the position, so navigation and collision are unaffected. The wheel only zooms while the canvas has focus, so a page that scrolls past it still scrolls, and ctrl/cmd with+/-stays the browser's own page zoom.zoomSpeed(default 2.5) is the factor per second of a held key and one wheel notch is a quarter of it;minZoom/maxZoom(0.8 to 4) bound the range and must contain 1.zoom={false}leavescamera.zoomalone and hands the wheel, the pinch and+/-back to the page.zoomReffills aRefObject<ZoomHandle | null>withlevel(),zoomIn(seconds?),zoomOut(seconds?),set(level)andreset(), for+/−buttons or a slider the space renders outside the Canvas — the only zoom a touch visitor has. It obeys the same range and keeps working underzoom={false}, which is how a space takes the input over entirely.onZoomreports the new level whenever it changes, whatever drove it, for a readout or a disabled button. Nothing is reported when a press cannot move the level.const zoom = useRef<ZoomHandle>(null); const [level, setLevel] = useState(1); // inside SpaceCanvas: <WalkController spawn={spawn} navigation={nav} zoomRef={zoom} onZoom={setLevel} /> <button onClick={() => zoom.current?.zoomIn()} disabled={level >= DEFAULT_MAX_ZOOM}> 확대 </button>;zoomStep,clampZoom,applyZoom: the step, clamp and camera write the controller itself uses, for a space doing something the handle does not cover. The level lives on the camera ascamera.zoom, where 1 is the space's ownfovframing and the levelresetKeyreturns to.DEFAULT_ZOOM_SPEED,DEFAULT_MIN_ZOOM,DEFAULT_MAX_ZOOMandWHEEL_NOTCH_SECONDSare those defaults;ZoomHandleis the handle type.walkStep,hitsCircle: independent collision math.MIN_RADIUS,MAX_STEPSbound the substep loop. It guarantees the visitor never enters an overlap; a position already overlapping an obstacle or outside the bounds may move freely until it is clear, so it can walk out instead of freezing.BasicLighting: configurable default lights (intensity,hemisphereIntensity); spaces can supply their own.Selectable: raycast selection callback with drag suppression. Space owns selected ID and UI.Model: cached uncompressed GLTF with per-instance skeleton cloning. Use Suspense + error boundary. DRACO/Meshopt are disabled; add a documented local decoder adapter if needed.assetUrl: prefix-preserving local asset reference.CameraPose,NavigationConfig,Footprint,RenderMetrics: public types.
Navigation requires valid bounds, positive obstacle dimensions, a collision-free spawn, finite movement inputs,
a visitor radius of at least MIN_RADIUS and a single move short enough to stay under MAX_STEPS substeps;
anything else throws instead of tunneling or stalling the frame. validateNavigation checks the whole config
up front, including obstacle dimensions that walkStep is too hot to re-check per frame. It supports one level. No stairs/slopes/physics. This is not a SceneBundle 1.0 parser; add an explicit adapter before using existing showroom bundles.
Release
- Change version and CHANGELOG.md in a reviewed PR; breaking public API changes require a major version even during 0.x for this team's policy.
- Keep stack.json, exact peers and dev dependencies consistent.
runtimeanddevelopmentbind core and spaces alike;coreandspacebind only their own side. Core test tooling belongs incore, space build tooling inspace— never indevelopment. Run the packed package against the template before release. - Merge to protected main, create an immutable GitHub release tag
v<package version>from that commit, then run the Publish workflow on main with that exact version. The workflow verifies the tag points to its commit. - Configure a
package-releaseenvironment restricted to main with reviewers. This repository's GITHUB_TOKEN needs package write permission; no human token is stored in the repository. - In package settings, grant Actions read access to each consuming repository. New versions require separate update PRs in spaces; reverting that PR restores the previous pinned release.
See the generated github-setup directory for initial GitHub setup. Before first publish, keep the repo private and retain UNLICENSED until the organization chooses a source license.
Changelog
0.3.0
WalkControllerzooms the view on the mouse wheel, a two-finger pinch and+/-, on by default for every space. It scalescamera.zoom, so the camera does not move and navigation, collision and bounds are untouched;resetKeyreturns the level to 1 with the rest of the view.- The wheel zooms only while the canvas has focus, so a page that scrolls past an unfocused canvas
still scrolls, and ctrl/cmd with
+/-is left to the browser's own page zoom. zoomSpeed,minZoom,maxZoomtune it andzoom={false}turns it off entirely. The range must contain 1, the space's own framing, and is checked on commit like the rest of the config.zoomRefhands a space aZoomHandle—level,zoomIn,zoomOut,set,reset— so+/−buttons, a slider or a reset rendered outside the Canvas drive the same clamped zoom without a bridge component. It keeps working underzoom={false}, for a space that owns the input itself.onZoomreports the new level whenever it changes, whatever drove it, and stays quiet when a press cannot move it, for a live readout or a disabled button.- New
zoomStep,clampZoomandapplyZoomexports withDEFAULT_ZOOM_SPEED,DEFAULT_MIN_ZOOM,DEFAULT_MAX_ZOOMandWHEEL_NOTCH_SECONDS. A zoom step is measured in seconds of a held key; one wheel notch, and one press of a space's button, is 0.25 of it.
0.2.0
WalkControllertakescollision.falsewalks through obstacles;navigation.boundsstill holds, so the visitor stays inside the space andresetKeyalways recovers.speedandturnSpeedare documented in m/s and rad/s and take effect on the next frame, so a control can drive them without remounting the controller or resetting the camera.walkStepenforces a constraint only while the current position already satisfies it. A visitor standing inside an obstacle, or outside the bounds, walks out instead of being frozen in every direction. The guarantee narrows from "never overlaps" to "never enters an overlap".- Korean 3D glossary in
docs/3d-glossary-ko.md, including the reasoning behind theSpaceCanvasdefaults fordpr,fov,nearandfar.
0.1.0
Initial generic Canvas, walking, collision, selection, GLTF loading and supported stack manifest.
WalkControllerresets onresetKeyor a changed spawn value, not on spawn object identity, so an inline spawn literal no longer snaps the camera back on every parent render.walkSteprejects a radius belowMIN_RADIUSand a move needing more thanMAX_STEPSsubsteps instead of stalling the frame in an unbounded loop.validateNavigationchecks bounds, obstacle dimensions and radius;WalkControllerruns it once on commit, so a bad config surfaces through an error boundary instead of throwing every frame.- Keyboard yaw (q/e) and pitch (r/f), so looking around no longer requires a pointer.
SpaceCanvastakeswebglFallback;BasicLightingtakeshemisphereIntensity.- Published
filesincludesrc, so the shipped declaration maps resolve. - Lint runs
eslint-plugin-react-hooksoversrcand fails on warnings. stack.jsonsplits intoruntime/development, which bind core and spaces alike, andcore/space, which bind only their own side, so core test tooling is no longer forced on every space.- Declaring an out-of-scope stack entry still pins it to the supported version.
@vitejs/plugin-reactreturns to the manifest as a space entry; removing it left the template's copy unpinned.- Core ships
scripts/check-stack.mjs; spaces run it fromnode_modulesinstead of keeping a copy that can drift from the manifest it checks.