Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Read on session start, in order:

# GitNexus — Code Intelligence

This project is indexed by GitNexus as **BitmapForge** (1798 symbols, 4337 relationships, 146 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **BitmapForge** (1755 symbols, 4499 relationships, 144 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
15 changes: 12 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ BitmapForge is a client-side web app that converts 3D models (STL, OBJ, GLTF/GLB
## Commands

- `npm run dev` — Start Vite dev server
- `npm run build` — Production build to `dist/`
- `npm run build` — Production build to `dist/` (also builds embed SDK first via `prebuild`)
- `npm run build:embed` — Build embed SDK → copies to `public/embed/`
- `npm run preview` — Preview production build
- `npm test` — Run all tests (Vitest)
- `npm run test:watch` — Run tests in watch mode
- `npm run test:e2e` — Run Playwright E2E tests
- `npm run coverage` — Generate coverage report
- `npm run lint` — Check for ESLint errors
- `npm run lint:fix` — Auto-fix ESLint errors
- `npm run format` — Prettier format all files
- `npm run format:check` — Check Prettier formatting

No test framework or linter is configured.
**Tooling:** ESLint + Prettier for code style, Vitest for unit/integration tests, Playwright for E2E. Pre-commit hooks (husky + lint-staged) run lint + format on staged files automatically.

## Architecture

Expand Down Expand Up @@ -58,7 +67,7 @@ React 19, Vite 7, Tailwind CSS 4, Zustand (state), Three.js (3D), Radix UI (prim

# GitNexus — Code Intelligence

This project is indexed by GitNexus as **BitmapForge** (1798 symbols, 4337 relationships, 146 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **BitmapForge** (1755 symbols, 4499 relationships, 144 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
6 changes: 0 additions & 6 deletions public/embed/bitmap-forge.es.js
Original file line number Diff line number Diff line change
Expand Up @@ -3083,7 +3083,6 @@ class $ {
applyReset(e, t) {
return !1;
}
// eslint-disable-line no-unused-vars
/** Cancel any in-progress reset. */
clearReset() {
}
Expand Down Expand Up @@ -4073,11 +4072,6 @@ const tt = {
helvetikerBold: () => import("./helvetiker_bold.typeface-Dpdv0pBL.js"),
optimer: () => import("./optimer_regular.typeface-BwmB6JhU.js"),
optimerBold: () => import("./optimer_bold.typeface-DdbaUQRb.js"),
gentilis: () => import("./gentilis_regular.typeface-D0DjsepO.js"),
gentilisBold: () => import("./gentilis_bold.typeface-6e94SVPf.js"),
droidSans: () => import("./droid_sans_regular.typeface-Bs81qZjP.js"),
droidSansBold: () => import("./droid_sans_bold.typeface-CSkX9gkA.js"),
droidSerif: () => import("./droid_serif_regular.typeface-fxu4RfN8.js"),
droidMono: () => import("./droid_sans_mono_regular.typeface-Bk29f_ZW.js")
}, Pn = new Ln(), Se = /* @__PURE__ */ new Map();
async function Nn(u) {
Expand Down
21 changes: 21 additions & 0 deletions src/app/components/AnimationControls/AnimationControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ function AnimationControls() {
const animationSpeed = useProjectStore((state) => state.animationSpeed)
const showPhaseDuration = useProjectStore((state) => state.showPhaseDuration)
const animationDuration = useProjectStore((state) => state.animationDuration)
const fadeMode = useProjectStore((state) => state.fadeMode)
const setUseFadeInOut = useProjectStore((state) => state.setUseFadeInOut)
const setFadeVariant = useProjectStore((state) => state.setFadeVariant)
const setFadeMode = useProjectStore((state) => state.setFadeMode)
const setAnimationEffect = useProjectStore((state) => state.setAnimationEffect)
const setAnimationSpeed = useProjectStore((state) => state.setAnimationSpeed)
const setShowPhaseDuration = useProjectStore((state) => state.setShowPhaseDuration)
Expand All @@ -39,6 +41,25 @@ function AnimationControls() {

{useFadeInOut && (
<div className="space-y-1.5 pl-1">
<span className="text-xs font-medium text-zinc-400">Direction</span>
<div className="flex flex-wrap gap-2">
{[
{ key: 'both', label: 'Both' },
{ key: 'in', label: 'Fade in only' },
{ key: 'out', label: 'Fade out only' }
].map(({ key, label }) => (
<label key={key} className="flex items-center gap-1.5 rounded bg-zinc-800 px-2 py-1 text-xs">
<input
type="radio"
name="fade-mode"
value={key}
checked={(fadeMode ?? 'both') === key}
onChange={() => setFadeMode(key)}
/>
{label}
</label>
))}
</div>
<span className="text-xs font-medium text-zinc-400">Style</span>
<div className="flex flex-wrap gap-2">
{FADE_VARIANT_KEYS.map((key) => (
Expand Down
1 change: 1 addition & 0 deletions src/app/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const selectEffectOptions = (state) => ({

export const selectAnimationOptions = (state) => ({
useFadeInOut: state.useFadeInOut,
fadeMode: state.fadeMode,
animationEffects: state.animationEffects,
animationSpeed: state.animationSpeed,
showPhaseDuration: state.showPhaseDuration,
Expand Down
6 changes: 4 additions & 2 deletions src/app/store/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ function makeState() {
colorShiftSaturation: 0,
// animation
useFadeInOut: true,
fadeMode: 'both',
animationEffects: { spinY: true },
animationSpeed: 1.0,
showPhaseDuration: 3000,
animationDuration: 2500,
animationPreset: 'spinY',
rotateOnShow: false,
showPreset: 'spinY',
fadeVariant: 'bloom',
fadeVariant: 'cascade',
// input source
inputType: 'model',
model: null,
Expand Down Expand Up @@ -103,10 +104,11 @@ describe('selectEffectOptions', () => {
})

describe('selectAnimationOptions', () => {
it('returns object with all 8 animation fields', () => {
it('returns object with all 9 animation fields', () => {
const result = selectAnimationOptions(makeState())
const keys = [
'useFadeInOut',
'fadeMode',
'animationEffects',
'animationSpeed',
'showPhaseDuration',
Expand Down
4 changes: 3 additions & 1 deletion src/app/store/slices/animationSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { DEFAULT_ANIMATION_EFFECTS } from '../../../engine/animation/effectTypes

export const ANIMATION_DEFAULTS = {
useFadeInOut: true,
fadeVariant: 'bloom',
fadeVariant: 'glitch',
fadeMode: 'both',
animationEffects: { ...DEFAULT_ANIMATION_EFFECTS },
animationSpeed: 1.0,
showPhaseDuration: 3000,
Expand All @@ -17,6 +18,7 @@ export const createAnimationSlice = (set, _get) => ({

setUseFadeInOut: (useFadeInOut) => set({ useFadeInOut }),
setFadeVariant: (fadeVariant) => set({ fadeVariant }),
setFadeMode: (fadeMode) => set({ fadeMode }),
setAnimationEffect: (key, value) =>
set((state) => ({
animationEffects: { ...state.animationEffects, [key]: Boolean(value) }
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/codeExport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('buildCodeZip — engine sources', () => {
it('ZIP contains 35 engine source files', async () => {
const zip = await getCodeZip()
const engineFiles = Object.keys(zip.files).filter((p) => p.includes('/engine/') && !p.endsWith('/'))
expect(engineFiles).toHaveLength(40)
expect(engineFiles).toHaveLength(41)
})

it('ZIP contains engine/SceneManager.js', async () => {
Expand Down
6 changes: 4 additions & 2 deletions src/app/utils/engineSources.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import BaseEffectSrc from '../../engine/effects/BaseEffect.js?raw'
import BitmapEffectSrc from '../../engine/effects/BitmapEffect.js?raw'
import ditherStrategiesSrc from '../../engine/effects/ditherStrategies.js?raw'
import BaseFadeVariantSrc from '../../engine/effects/fadeVariants/BaseFadeVariant.js?raw'
import BloomVariantSrc from '../../engine/effects/fadeVariants/BloomVariant.js?raw'
import CascadeVariantSrc from '../../engine/effects/fadeVariants/CascadeVariant.js?raw'
import StaticVariantSrc from '../../engine/effects/fadeVariants/StaticVariant.js?raw'
import GlitchVariantSrc from '../../engine/effects/fadeVariants/GlitchVariant.js?raw'
import DriftVariantSrc from '../../engine/effects/fadeVariants/DriftVariant.js?raw'
import ScatterVariantSrc from '../../engine/effects/fadeVariants/ScatterVariant.js?raw'
import fadeVariantsIndexSrc from '../../engine/effects/fadeVariants/index.js?raw'
import BaseRendererSrc from '../../engine/renderers/BaseRenderer.js?raw'
import BitmapRendererSrc from '../../engine/renderers/BitmapRenderer.js?raw'
Expand Down Expand Up @@ -57,10 +58,11 @@ const ENGINE_SOURCES = [
{ path: 'engine/effects/BitmapEffect.js', content: BitmapEffectSrc },
{ path: 'engine/effects/ditherStrategies.js', content: ditherStrategiesSrc },
{ path: 'engine/effects/fadeVariants/BaseFadeVariant.js', content: BaseFadeVariantSrc },
{ path: 'engine/effects/fadeVariants/BloomVariant.js', content: BloomVariantSrc },
{ path: 'engine/effects/fadeVariants/CascadeVariant.js', content: CascadeVariantSrc },
{ path: 'engine/effects/fadeVariants/StaticVariant.js', content: StaticVariantSrc },
{ path: 'engine/effects/fadeVariants/GlitchVariant.js', content: GlitchVariantSrc },
{ path: 'engine/effects/fadeVariants/DriftVariant.js', content: DriftVariantSrc },
{ path: 'engine/effects/fadeVariants/ScatterVariant.js', content: ScatterVariantSrc },
{ path: 'engine/effects/fadeVariants/index.js', content: fadeVariantsIndexSrc },
{ path: 'engine/renderers/BaseRenderer.js', content: BaseRendererSrc },
{ path: 'engine/renderers/BitmapRenderer.js', content: BitmapRendererSrc },
Expand Down
6 changes: 4 additions & 2 deletions src/app/utils/engineSources.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ const REQUIRED_PATHS = [
'engine/effects/BitmapEffect.js',
'engine/effects/ditherStrategies.js',
'engine/effects/fadeVariants/BaseFadeVariant.js',
'engine/effects/fadeVariants/BloomVariant.js',
'engine/effects/fadeVariants/CascadeVariant.js',
'engine/effects/fadeVariants/StaticVariant.js',
'engine/effects/fadeVariants/GlitchVariant.js',
'engine/effects/fadeVariants/DriftVariant.js',
'engine/effects/fadeVariants/ScatterVariant.js',
'engine/effects/fadeVariants/index.js',
'engine/renderers/BaseRenderer.js',
'engine/renderers/BitmapRenderer.js',
Expand Down Expand Up @@ -99,10 +100,11 @@ describe('ENGINE_SOURCES', () => {
it('includes all fadeVariant files', () => {
const fadeFiles = [
'engine/effects/fadeVariants/BaseFadeVariant.js',
'engine/effects/fadeVariants/BloomVariant.js',
'engine/effects/fadeVariants/CascadeVariant.js',
'engine/effects/fadeVariants/StaticVariant.js',
'engine/effects/fadeVariants/GlitchVariant.js',
'engine/effects/fadeVariants/DriftVariant.js',
'engine/effects/fadeVariants/ScatterVariant.js',
'engine/effects/fadeVariants/index.js'
]
const paths = new Set(ENGINE_SOURCES.map((e) => e.path))
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/exportConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function makeState(overrides = {}) {
minBrightness: 0.05,
backgroundColor: '#0a0a0a',
animationDuration: 2500,
fadeVariant: 'bloom',
fadeVariant: 'cascade',
renderMode: 'bitmap',
model: null,
useFadeInOut: true,
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/projectFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function migrateV1toV2(project) {
settings: {
...project.settings,
useFadeInOut: project.settings.useFadeInOut ?? true,
fadeVariant: project.settings.fadeVariant ?? 'bloom',
fadeVariant: project.settings.fadeVariant ?? 'cascade',
animationEffects: project.settings.animationEffects ?? { ...DEFAULT_ANIMATION_EFFECTS },
baseRotation: project.settings.baseRotation ?? { x: 0, y: 0, z: 0 },
seed: null // preserve old deterministic-hash behavior
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/reactComponentExport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const BASE_STATE = {
minBrightness: 0.05,
backgroundColor: '#0a0a0a',
animationDuration: 2500,
fadeVariant: 'bloom',
fadeVariant: 'cascade',
useFadeInOut: true,
animationEffects: { spinX: false, spinY: true, spinZ: false, float: false },
animationSpeed: 1.0,
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('buildReactComponent — ZIP structure', () => {
it('ZIP contains 35 engine source files', async () => {
const zip = await getZip()
const engineFiles = Object.keys(zip.files).filter((p) => p.includes('/engine/') && !p.endsWith('/'))
expect(engineFiles).toHaveLength(40)
expect(engineFiles).toHaveLength(41)
})

it('uses custom component name for folder', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/webComponentExport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const BASE_STATE = {
minBrightness: 0.05,
backgroundColor: '#0a0a0a',
animationDuration: 2500,
fadeVariant: 'bloom',
fadeVariant: 'cascade',
useFadeInOut: true,
animationEffects: { spinX: false, spinY: true, spinZ: false, float: false },
animationSpeed: 1.0,
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('buildWebComponent — ZIP structure', () => {
it('ZIP contains 35 engine source files', async () => {
const zip = await getZip()
const engineFiles = Object.keys(zip.files).filter((p) => p.includes('/engine/') && !p.endsWith('/'))
expect(engineFiles).toHaveLength(40)
expect(engineFiles).toHaveLength(41)
})

it('uses custom element name for folder and file', async () => {
Expand Down
6 changes: 6 additions & 0 deletions src/engine/SceneManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ class SceneManager {

this.animationEngine = new AnimationEngine()

// When the user switches fade variant mid-loop, reset the model rotation so the
// new fadeIn always captures the front-facing pose.
this.effect.onVariantChange = () => {
this.animationEngine.onExternalFadeRestart(this.animGroup)
}

this.objectGroup = null
this.currentObjectUrl = null
this.lastFrameTime = performance.now()
Expand Down
Loading
Loading