@vgai/editor 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -1,86 +0,0 @@
1
- import { GameComponent } from '@engine/ecs/game-component';
2
- import type { GameContext } from '@engine/runtime/types';
3
- import type * as THREE from 'three';
4
- import { z } from 'zod';
5
- import { tuning } from '../../data/tuning';
6
- import { advanceLap } from './spin-lap-math';
7
-
8
- /**
9
- * The template's worked data-asset consumer (docs/DATA-TOOLS-DESIGN.md §6.2
10
- * — the pattern ships working at birth): spins the default scene's Box at
11
- * `boxSpinSpeed` and tints it `boxColor`, both read from
12
- * `src/data/tuning.data.json` through the `tuning` handle.
13
- *
14
- * Try it: press ▶, then edit `src/data/tuning.data.json` and save — the box
15
- * changes speed/color LIVE (Vite HMR, no reload; see src/data/tuning.ts).
16
- *
17
- * This component demonstrates BOTH halves of the data-vs-config split
18
- * (§3.3/§6.2): `boxSpinSpeed`/`boxColor` are PROJECT-WIDE feel, so they live
19
- * in the data asset and read through `tuning.get()` (every frame — cache a
20
- * field across frames and live tuning silently stops reaching it). `spinScale`
21
- * is THIS entity's own knob — per-entity config authored on the component
22
- * itself via `static schema` (the same shape SceneCamera uses), read live by
23
- * an inspector tool through `useSelection()` (see `spin.tool.tsx`).
24
- *
25
- * It's also the template's worked debug-seam consumer (`ctx.debug`,
26
- * `docs/SYNTHETIC-PLAYER-SPEC.md`/`docs/ACCEPTANCE-DRIVER-SPEC.md`, Task
27
- * 4.2): holding the `attack` action (see `public/inputmaps/default.inputmap.json`)
28
- * boosts the spin, and each completed lap emits a `spin-lap` event —
29
- * `tests/acceptance/example.spec.ts` drives `attack` through the HONEST input
30
- * path and asserts on the resulting `spinner` state/events; `spinner.reset`
31
- * is a declared fixture, never a substitute for that proof (D16).
32
- */
33
- export class DataSpinner extends GameComponent {
34
- static schema = z.object({
35
- spinScale: z
36
- .number()
37
- .default(1)
38
- .describe(
39
- 'Per-entity multiplier on the shared boxSpinSpeed — the project-wide feel stays in ' +
40
- "src/data/tuning.data.json; this is the one knob that is THIS entity's own.",
41
- ),
42
- });
43
-
44
- spinScale = 1;
45
- private rotationSinceLap = 0;
46
- private lapsCompleted = 0;
47
-
48
- init(ctx: GameContext) {
49
- ctx.debug?.registerStateProvider(
50
- 'spinner',
51
- () => ({ lapsCompleted: this.lapsCompleted, spinScale: this.spinScale }),
52
- { tier: 'observable' },
53
- );
54
- // Fixture, never a proof (D16): lets a test jump straight to a known
55
- // lapsCompleted instead of waiting out real spin time. The acceptance
56
- // spec's actual PROOF is driving 'attack' through game.input.hold and
57
- // asserting on the observable 'spinner' state/'spin-lap' events below.
58
- ctx.debug?.registerCommand(
59
- 'spinner.reset',
60
- { description: 'Reset lapsCompleted to 0.', locus: 'client' },
61
- () => {
62
- this.lapsCompleted = 0;
63
- this.rotationSinceLap = 0;
64
- },
65
- );
66
- }
67
-
68
- update(dt: number, ctx: GameContext) {
69
- const t = tuning.get();
70
- const boost = ctx.input.isPressed('attack') ? 4 : 1;
71
- const delta = t.boxSpinSpeed * this.spinScale * boost * dt;
72
- this.object3D.rotation.y += delta;
73
-
74
- const progress = advanceLap(this.rotationSinceLap, delta);
75
- this.rotationSinceLap = progress.rotationSinceLap;
76
- if (progress.lapsCompleted > 0) {
77
- this.lapsCompleted += progress.lapsCompleted;
78
- ctx.debug?.emit('spin-lap', { lapsCompleted: this.lapsCompleted });
79
- }
80
-
81
- const material = (this.object3D as THREE.Mesh).material as
82
- | THREE.MeshStandardMaterial
83
- | undefined;
84
- if (material?.color) material.color.set(t.boxColor);
85
- }
86
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Pure lap-counting math for `DataSpinner` (`data-spinner.ts`) — factored out
3
- * into its own zero-dependency module so `tests/logic/example.test.ts` can
4
- * exercise the actual rule the component ships with, with no `@engine`/DOM/
5
- * Playwright dependency: the L1 layer (`docs/ACCEPTANCE-DRIVER-SPEC.md` §0 —
6
- * "never debug a pure-model bug through the browser").
7
- */
8
-
9
- const FULL_ROTATION = Math.PI * 2;
10
-
11
- export interface LapProgress {
12
- /** Radians accumulated since the last completed lap, always in `[0, 2π)`. */
13
- rotationSinceLap: number;
14
- /** How many full 2π rotations `delta` pushed past on this call (0, 1, 2, ...). */
15
- lapsCompleted: number;
16
- }
17
-
18
- /** Advance `rotationSinceLap` by `delta` radians, rolling over into completed laps. */
19
- export function advanceLap(rotationSinceLap: number, delta: number): LapProgress {
20
- let next = rotationSinceLap + delta;
21
- let lapsCompleted = 0;
22
- while (next >= FULL_ROTATION) {
23
- next -= FULL_ROTATION;
24
- lapsCompleted++;
25
- }
26
- return { rotationSinceLap: next, lapsCompleted };
27
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Example project tool (a center-document tool) — the worked pattern
3
- * (docs/DATA-TOOLS-DESIGN.md §3, in the engine checkout). Slightly
4
- * over-commented on purpose: this file is read far more than it runs.
5
- *
6
- * A tool is ONE file, `src/tools/<name>.tool.tsx`, exporting exactly two
7
- * things:
8
- * 1. `tool` — metadata via `defineTool` (id, tab title, placement),
9
- * 2. a default-exported plain React component.
10
- * The editor discovers it automatically (folder scan — no registration
11
- * anywhere; delete the file and it's gone) and opens it as a CENTER
12
- * workspace document via the command palette's `Open Tool: Tuning` action
13
- * (`placement: 'document'`; the older `placement: 'dock'` spelling preserves
14
- * its historical bottom-panel placement; a `placement: 'utility'` tool also
15
- * renders as a bottom utility). It stays interactive during play mode, so
16
- * you tune while the game runs.
17
- *
18
- * Tools are EDITOR-ONLY dev surfaces: nothing in game code imports
19
- * `src/tools/`, so `npm run build` never bundles this file — cheats and
20
- * tuning panels can't leak into a shipped game. Keep game logic out of here.
21
- *
22
- * This example closes the full live-tuning loop on the `tuning` data asset
23
- * (`src/data/` — the trio this project ships working):
24
- *
25
- * slider → writeData() → src/data/tuning.data.json written on disk
26
- * → Vite HMR → tuning.hotSwap() → useData re-renders this panel
27
- * → and the RUNNING game (DataSpinner reads tuning.get() per frame)
28
- * picks the value up the same instant, no reload.
29
- *
30
- * That's the whole architecture: the file is the document; the editor, this
31
- * panel, VS Code, and the game are all just views of it.
32
- */
33
-
34
- import { useData, writeData } from '@engine/react/use-data';
35
- import { defineTool } from '@engine/tools/define-tool';
36
- // A tool imports the project's OWN data handles — the same ones the game
37
- // reads. One handle, every surface agrees on the values.
38
- import { tuning } from '../data/tuning';
39
-
40
- /** The editor reads this export to place the tool (docs/DATA-TOOLS-DESIGN.md
41
- * §3.1). `id` keys the document; `title` is the tab/action label;
42
- * `'document'` is the project-global placement — it opens as a center
43
- * document (for the selection-scoped `'inspector'` placement, see
44
- * `spin.tool.tsx`; transient output panels declare `'utility'` and render
45
- * as a bottom tab). `'dock'` is a deprecated compatibility spelling for a
46
- * bottom utility — supported so existing tools keep their behavior. */
47
- export const tool = defineTool({
48
- id: 'tuning',
49
- title: 'Tuning',
50
- placement: 'document',
51
- });
52
-
53
- /** The panel itself: plain React, inline styles (editor chrome around it is
54
- * editor-styled; what's inside is yours). No providers needed. */
55
- export default function TuningTool() {
56
- // useData = live values: re-renders once per accepted edit of the data
57
- // file, whoever made it (this slider, the editor's Data tab, VS Code, an
58
- // agent). Read it every render — never copy fields into component state.
59
- const values = useData(tuning);
60
-
61
- return (
62
- <div style={{ padding: 12, fontSize: 12, fontFamily: 'ui-monospace, monospace' }}>
63
- {/* Live readout — proves the round trip: drag the slider (or edit the
64
- JSON anywhere else) and watch these values follow the file. */}
65
- <div style={{ marginBottom: 10, color: '#8c8c8c' }}>
66
- src/data/tuning.data.json (live):
67
- <pre style={{ margin: '4px 0 0', color: 'inherit' }}>{JSON.stringify(values, null, 2)}</pre>
68
- </div>
69
-
70
- <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
71
- boxSpinSpeed
72
- <input
73
- type="range"
74
- min={-5}
75
- max={5}
76
- step={0.1}
77
- data-testid="tool-slider-boxSpinSpeed"
78
- // Uncontrolled (defaultValue) on purpose: the authoritative value
79
- // round-trips through the FILE (write → HMR → useData), which can
80
- // lag a beat behind the pointer — controlling the thumb from that
81
- // would make it fight the drag. The readout above is the live view.
82
- defaultValue={values.boxSpinSpeed}
83
- // writeData writes the WHOLE file through the editor dev server
84
- // (only src/data/**/*.data.json is accepted): spread the current
85
- // values, replace one field. It's async — fire and forget here.
86
- onChange={(e) =>
87
- void writeData('src/data/tuning.data.json', {
88
- ...values,
89
- boxSpinSpeed: Number(e.target.value),
90
- })
91
- }
92
- />
93
- <span>{values.boxSpinSpeed}</span>
94
- </label>
95
- </div>
96
- );
97
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Example INSPECTOR tool — the selection-scoped project-tool pattern
3
- * (docs/DATA-TOOLS-DESIGN.md §3, W3/W3.1, in the engine checkout). Slightly
4
- * over-commented on purpose: this file is read far more than it runs.
5
- *
6
- * Same one-file physics as the dock example (`example.tool.tsx`), different
7
- * placement: `placement: 'inspector'` + a `match` predicate. Instead of a
8
- * bottom-dock tab, the editor renders this as a ⚙-marked SECTION in the
9
- * Inspector rail whenever the current selection satisfies `match` — select
10
- * the Box (it has a `DataSpinner`) and it appears under the built-in
11
- * sections; select the Ground (a mesh, but no `DataSpinner`) or a light and
12
- * it's gone. It stays live during play mode, so you tune while the game runs.
13
- *
14
- * Three things this file demonstrates beyond the dock example:
15
- *
16
- * 1. `match(node, adapter)` — the selection predicate, now COMPONENT-based
17
- * rather than shape-based: it asks `adapter.components?.list(node.id)`
18
- * for the attached component types instead of guessing from `node.kind`.
19
- * Keep it cheap and pure, it runs on every inspector render.
20
- * 2. `useSelection()` (`@engine/react/use-selection`, W3.1) — inside the
21
- * component, this returns the selected node PLUS its attached components
22
- * and each one's own authored config (`ComponentsProvider.config`). Here
23
- * it reads `DataSpinner`'s per-entity `spinScale` — read-only in v1;
24
- * per-entity WRITE-from-tool is future `useTunable`/inspector-set
25
- * territory. `useSelection` only works inside an inspector tool (it
26
- * throws a teaching error from a dock tool — there is no selection there).
27
- * 3. `@editor/widgets` — the editor's OWN widget kit + design tokens,
28
- * public for project tools (§3.4 "tools are editor citizens"): panels
29
- * built from it sit flush with the built-in inspector sections and
30
- * re-skin with the editor. `NumberInput` here is the exact
31
- * scrub-or-type field the built-in Transform section uses.
32
- *
33
- * The component receives `{ node, nodeId }` as props (see
34
- * `InspectorToolProps` in `@engine/tools/define-tool`) — unchanged by W3.1;
35
- * richer data arrives through `useSelection()`, not more props. Like every
36
- * tool, it's editor-only: `npm run build` never bundles this file.
37
- */
38
-
39
- import { NumberInput, THEME } from '@editor/widgets';
40
- import { useData, writeData } from '@engine/react/use-data';
41
- import { useSelection } from '@engine/react/use-selection';
42
- import { defineTool, type InspectorToolProps } from '@engine/tools/define-tool';
43
- // The same project data handle the game (DataSpinner) and the dock example
44
- // read — one handle, every surface agrees on the values.
45
- import { tuning } from '../data/tuning';
46
-
47
- export const tool = defineTool({
48
- id: 'spin',
49
- title: 'Spin tuning',
50
- placement: 'inspector',
51
- // Show this section only on an entity with a DataSpinner attached — the
52
- // component-based idiom (rather than guessing from `node.kind`). `node` is
53
- // null in the no-selection state — return false to stay out of the panel.
54
- match: (node, adapter) =>
55
- node !== null &&
56
- (adapter.components?.list(node.id) ?? []).some((c) => c.type === 'DataSpinner'),
57
- });
58
-
59
- export default function SpinInspector({ node }: InspectorToolProps) {
60
- // Live values: re-renders on every accepted edit of the data file,
61
- // whoever made it (this field, the dock tool, the DATA tab, VS Code).
62
- const values = useData(tuning);
63
-
64
- // The selected entity's own components + their authored config, live
65
- // (W3.1) — distinct from `values` above: `tuning` is project-wide feel,
66
- // `spinScale` here is THIS entity's own per-entity knob.
67
- const { components } = useSelection();
68
- const spinner = components.find((c) => c.type === 'DataSpinner');
69
- const spinScale =
70
- typeof spinner?.config?.['spinScale'] === 'number'
71
- ? (spinner.config['spinScale'] as number)
72
- : 1;
73
-
74
- return (
75
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11 }}>
76
- <div style={{ color: THEME.textMuted }}>
77
- {node ? `Selected: ${node.label}` : 'Nothing selected'} — boxSpinSpeed drives the Box's
78
- DataSpinner (src/data/tuning.data.json, live)
79
- </div>
80
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
81
- <span style={{ color: THEME.textMuted, width: 90 }}>boxSpinSpeed</span>
82
- {/* The editor's own scrub-or-type number field (@editor/widgets):
83
- drag horizontally to scrub, click to type. writeData writes the
84
- whole data file through the dev server; HMR flows it back into
85
- useData above AND the running game. */}
86
- <NumberInput
87
- value={values.boxSpinSpeed}
88
- step={0.1}
89
- testId="spin-tool-speed"
90
- style={{ flex: 1 }}
91
- onChange={(v) =>
92
- void writeData('src/data/tuning.data.json', { ...values, boxSpinSpeed: v })
93
- }
94
- />
95
- </div>
96
- <div style={{ color: THEME.textMuted }} data-testid="spin-tool-entity-scale">
97
- this entity's spinScale: {spinScale} ×
98
- </div>
99
- </div>
100
- );
101
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * L1 — logic layer (`docs/ACCEPTANCE-DRIVER-SPEC.md` §0): pure rules/math in
3
- * plain Node tests, seconds per run, never through a browser. This exercises
4
- * the ACTUAL lap-counting rule `DataSpinner` (`src/scripts/components/
5
- * data-spinner.ts`) ships with — imported from its real module, not
6
- * reimplemented here — so a regression in the rule fails here first, not in
7
- * the slower L2 acceptance spec (`tests/acceptance/example.spec.ts`).
8
- */
9
-
10
- import { describe, expect, it } from 'vitest';
11
- import { advanceLap } from '../../src/scripts/components/spin-lap-math';
12
-
13
- describe('advanceLap', () => {
14
- it('accumulates rotation without completing a lap below 2π', () => {
15
- const result = advanceLap(0, 1);
16
- expect(result.lapsCompleted).toBe(0);
17
- expect(result.rotationSinceLap).toBeCloseTo(1, 10);
18
- });
19
-
20
- it('completes exactly one lap and carries the remainder', () => {
21
- const result = advanceLap(0, Math.PI * 2 + 1);
22
- expect(result.lapsCompleted).toBe(1);
23
- expect(result.rotationSinceLap).toBeCloseTo(1, 10);
24
- });
25
-
26
- it('completes multiple laps in a single large step', () => {
27
- const result = advanceLap(0, Math.PI * 2 * 3.5);
28
- expect(result.lapsCompleted).toBe(3);
29
- expect(result.rotationSinceLap).toBeCloseTo(Math.PI, 10);
30
- });
31
- });