castle-web-cli 0.4.128 → 0.4.130

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 (157) hide show
  1. package/dist/agent-prompts.js +4 -4
  2. package/dist/filesChanged.d.ts +2 -0
  3. package/dist/filesChanged.js +6 -2
  4. package/dist/ide.js +15 -4
  5. package/dist/importBrowse.js +1 -0
  6. package/dist/index.js +10 -4
  7. package/dist/init.js +63 -15
  8. package/dist/native/loop.js +1 -1
  9. package/dist/native/tools.js +1 -1
  10. package/dist/serve.d.ts +1 -0
  11. package/dist/serve.js +20 -1
  12. package/dist/shell/assets/index-BpOLUyVO.js +441 -0
  13. package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/physics-2d/CLAUDE.md +40 -23
  16. package/kits/physics-2d/behaviors/Joints.jsx +4 -3
  17. package/kits/physics-2d/behaviors/SoundPlayer.jsx +266 -0
  18. package/kits/physics-2d/behaviors/Sprite.jsx +3 -3
  19. package/kits/physics-2d/behaviors/Video.jsx +4 -3
  20. package/kits/physics-2d/castle.json +1 -1
  21. package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
  22. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  23. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  24. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  25. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  26. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  27. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  28. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  29. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  30. package/kits/physics-2d/engine/audioContext.js +41 -0
  31. package/kits/physics-2d/engine/autoInspector.jsx +50 -33
  32. package/kits/physics-2d/engine/blueprint.js +15 -3
  33. package/kits/physics-2d/engine/liveReload.js +33 -21
  34. package/kits/physics-2d/engine/media.js +29 -39
  35. package/kits/physics-2d/engine/sound.js +316 -0
  36. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  37. package/kits/physics-2d/engine/ui.jsx +274 -5
  38. package/kits/physics-2d/engine/ui.module.css +137 -16
  39. package/kits/physics-2d/scripts/testsounds.mjs +84 -0
  40. package/kits/physics-2d/systems/media.js +31 -15
  41. package/kits/physics-3d/CLAUDE.md +109 -0
  42. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  43. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  44. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  45. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  46. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  47. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  48. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  49. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  50. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  51. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  52. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  53. package/kits/physics-3d/blueprints/crate.scene +29 -0
  54. package/kits/physics-3d/blueprints/door.scene +35 -0
  55. package/kits/physics-3d/blueprints/gem.scene +26 -0
  56. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  57. package/kits/physics-3d/blueprints/platform.scene +23 -0
  58. package/kits/physics-3d/blueprints/player.scene +29 -0
  59. package/kits/physics-3d/blueprints/portal.scene +21 -0
  60. package/kits/physics-3d/blueprints/rock.scene +14 -0
  61. package/kits/physics-3d/blueprints/statue.scene +26 -0
  62. package/kits/physics-3d/blueprints/tree.scene +14 -0
  63. package/kits/physics-3d/blueprints/wall.scene +26 -0
  64. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  65. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  66. package/kits/physics-3d/drawings/crate.pxart +26 -0
  67. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  68. package/kits/physics-3d/drawings/door.pxart +27 -0
  69. package/kits/physics-3d/drawings/face.pxart +26 -0
  70. package/kits/physics-3d/drawings/floor.pxart +27 -0
  71. package/kits/physics-3d/drawings/platform.pxart +27 -0
  72. package/kits/physics-3d/drawings/statue.pxart +27 -0
  73. package/kits/physics-3d/drawings/wall.pxart +27 -0
  74. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
  75. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  76. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  77. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  78. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  79. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  80. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  81. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  82. package/kits/physics-3d/engine3d/materials.js +174 -0
  83. package/kits/physics-3d/engine3d/meshops.js +123 -0
  84. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  85. package/kits/physics-3d/engine3d/modelFiles.js +139 -0
  86. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  87. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  88. package/kits/physics-3d/engine3d/world3d.js +216 -0
  89. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  90. package/kits/physics-3d/index.html +20 -0
  91. package/kits/physics-3d/main.jsx +30 -0
  92. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  93. package/kits/physics-3d/models/player.pxmodel +222 -0
  94. package/kits/physics-3d/models/rock.pxmodel +30 -0
  95. package/kits/physics-3d/models/tree.pxmodel +10 -0
  96. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  97. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  98. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  99. package/kits/physics-3d/scenes/main.scene +414 -0
  100. package/kits/physics-3d/scenes/model-lab.scene +127 -0
  101. package/kits/physics-3d/systems/physics3d.js +356 -0
  102. package/package.json +5 -2
  103. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  104. package/kits/basic-2d/CLAUDE.md +0 -221
  105. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  106. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  107. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  108. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  109. package/kits/basic-2d/behaviors/tint.js +0 -47
  110. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  111. package/kits/basic-2d/docs/pxart-format.md +0 -377
  112. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  113. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  114. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  115. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  116. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  117. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  118. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  119. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  120. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  121. package/kits/basic-2d/editors/editorHistory.js +0 -157
  122. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  123. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  124. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  125. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  126. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  127. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  128. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  129. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  130. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  131. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  132. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  133. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  134. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  135. package/kits/basic-2d/engine/assets.js +0 -15
  136. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  137. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  138. package/kits/basic-2d/engine/blueprint.js +0 -557
  139. package/kits/basic-2d/engine/collider.js +0 -200
  140. package/kits/basic-2d/engine/files.js +0 -141
  141. package/kits/basic-2d/engine/liveReload.js +0 -88
  142. package/kits/basic-2d/engine/pxart.js +0 -1032
  143. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  144. package/kits/basic-2d/engine/scene.js +0 -696
  145. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  146. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  147. package/kits/basic-2d/engine/ui.jsx +0 -695
  148. package/kits/basic-2d/engine/ui.module.css +0 -2287
  149. package/kits/basic-2d/index.html +0 -24
  150. package/kits/basic-2d/main.jsx +0 -24
  151. package/kits/basic-2d/scenes/main.scene +0 -16
  152. package/kits/basic-2d/scripts/draw.mjs +0 -121
  153. package/kits/physics-2d/behaviors/Sound.jsx +0 -163
  154. package/kits/physics-2d/behaviors/Tone.jsx +0 -95
  155. package/kits/physics-2d/engine/tone.js +0 -112
  156. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  157. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -0,0 +1,309 @@
1
+ // 3d scene player: mounts a SceneRuntime (from the imported physics-2d kit's
2
+ // engine -- the actor/behavior model is shared) against a three.js world.
3
+ // Behaviors update, the physics3d system steps rapier, then Transforms sync to
4
+ // three nodes and the composer renders. No game logic here.
5
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
6
+ import { makeScene } from '@imports/castle.physics-2d/engine/scene';
7
+ import {
8
+ makeWorld3D,
9
+ resizeWorld,
10
+ syncActors,
11
+ renderWorld,
12
+ disposeWorld,
13
+ updatePhysicsDebug,
14
+ } from './world3d';
15
+
16
+ export function Scene3DPlayer({
17
+ sceneData,
18
+ sprites,
19
+ files,
20
+ behaviorClasses,
21
+ scenePath,
22
+ onFirstFrame,
23
+ }) {
24
+ const canvasRef = useRef(null);
25
+ const scoreRef = useRef(null);
26
+ const lookRef = useRef(null);
27
+ const [error, setError] = useState(null);
28
+ const [runId, setRunId] = useState(0);
29
+
30
+ useEffect(() => {
31
+ const canvas = canvasRef.current;
32
+ if (!canvas) return undefined;
33
+ const runtime = makeScene(sceneData, behaviorClasses, sprites, files);
34
+ const world = makeWorld3D(canvas);
35
+ runtime.three = world;
36
+ runtime.hud = {};
37
+ // Scene-independent state: survives `loadFromFile` (the scene data is
38
+ // replaced, the runtime instance is not). Player score/inventory and the
39
+ // camera angle live here so they cross scene switches.
40
+ runtime.persistent = {};
41
+ runtime.currentScenePath = scenePath ?? 'scenes/main.scene';
42
+
43
+ const detachInput = attachInput(canvas, runtime, world);
44
+ const stopResize = attachResize(canvas, world);
45
+ const onLoopError = (err) => {
46
+ console.error(err);
47
+ setError(err);
48
+ };
49
+ const hudRefs = { score: scoreRef, look: lookRef };
50
+ const stopLoop = startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onLoopError);
51
+ return () => {
52
+ stopLoop();
53
+ stopResize();
54
+ detachInput();
55
+ runtime.dispose();
56
+ disposeWorld(world);
57
+ };
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [runId]);
60
+
61
+ const onRestart = useCallback(() => {
62
+ setError(null);
63
+ setRunId((n) => n + 1);
64
+ }, []);
65
+
66
+ return (
67
+ <div style={{ position: 'fixed', inset: 0, background: '#000' }}>
68
+ <canvas
69
+ ref={canvasRef}
70
+ tabIndex={0}
71
+ style={{
72
+ width: '100%',
73
+ height: '100%',
74
+ display: 'block',
75
+ outline: 'none',
76
+ touchAction: 'none',
77
+ userSelect: 'none',
78
+ WebkitUserSelect: 'none',
79
+ WebkitTouchCallout: 'none',
80
+ }}
81
+ />
82
+ <div ref={scoreRef} style={hudStyles.score} />
83
+ <div ref={lookRef} style={hudStyles.look} />
84
+ {error ? <LoopErrorOverlay error={error} onRestart={onRestart} /> : null}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ function attachInput(canvas, runtime, world) {
90
+ const onKeyDown = (event) => {
91
+ // Space is gameplay (jump); keep it from scrolling the page.
92
+ if (event.code === 'Space' && event.target === canvas) event.preventDefault();
93
+ if (event.code === 'KeyP' && !event.repeat) {
94
+ world.debugLines.visible = !world.debugLines.visible;
95
+ }
96
+ runtime.keys.add(event.code);
97
+ runtime.keys.add(event.key);
98
+ };
99
+ const onKeyUp = (event) => {
100
+ runtime.keys.delete(event.code);
101
+ runtime.keys.delete(event.key);
102
+ };
103
+ // Pointer (mouse or touch) input is multitouch, tracked per pointerId:
104
+ // - a DRAG is the virtual stick -- the offset from where the drag started
105
+ // maps to a walk direction (drag up = forward). The first pointer to
106
+ // travel past the tap slop owns the stick until it lifts.
107
+ // - a TAP (down and up quickly without moving much) is a jump, even while
108
+ // another finger is dragging. `moveInput.consumeJump()` hands the
109
+ // pending tap to the Player behavior exactly once.
110
+ // `runtime.moveInput` is what the Player behavior reads.
111
+ const STICK_RADIUS_PX = 56;
112
+ const TAP_SLOP_PX = 12;
113
+ const TAP_MS = 300;
114
+ const pointers = new Map();
115
+ let stickId = null;
116
+ let jumpPending = false;
117
+ const move = {
118
+ x: 0,
119
+ y: 0,
120
+ active: false,
121
+ consumeJump() {
122
+ const had = jumpPending;
123
+ jumpPending = false;
124
+ return had;
125
+ },
126
+ };
127
+ runtime.moveInput = move;
128
+ const clearStick = () => {
129
+ stickId = null;
130
+ move.active = false;
131
+ move.x = 0;
132
+ move.y = 0;
133
+ };
134
+ const onPointerDown = (event) => {
135
+ canvas.focus();
136
+ pointers.set(event.pointerId, {
137
+ x: event.clientX,
138
+ y: event.clientY,
139
+ time: performance.now(),
140
+ moved: false,
141
+ });
142
+ canvas.setPointerCapture(event.pointerId);
143
+ };
144
+ const onPointerMove = (event) => {
145
+ const p = pointers.get(event.pointerId);
146
+ if (!p) return;
147
+ const dx = event.clientX - p.x;
148
+ const dy = event.clientY - p.y;
149
+ if (!p.moved && Math.hypot(dx, dy) > TAP_SLOP_PX) p.moved = true;
150
+ // A pointer claims the stick once it is clearly a drag (and nothing else
151
+ // holds it) -- so a tapping finger never yanks the walk direction.
152
+ if (p.moved && stickId === null) stickId = event.pointerId;
153
+ if (stickId !== event.pointerId) return;
154
+ const nx = dx / STICK_RADIUS_PX;
155
+ const ny = dy / STICK_RADIUS_PX;
156
+ const len = Math.hypot(nx, ny);
157
+ const scale = len > 1 ? 1 / len : 1;
158
+ move.active = true;
159
+ move.x = nx * scale;
160
+ move.y = ny * scale;
161
+ };
162
+ const onPointerUp = (event) => {
163
+ const p = pointers.get(event.pointerId);
164
+ pointers.delete(event.pointerId);
165
+ if (stickId === event.pointerId) clearStick();
166
+ if (event.type === 'pointercancel' || !p) return;
167
+ if (!p.moved && performance.now() - p.time < TAP_MS) jumpPending = true;
168
+ };
169
+ window.addEventListener('keydown', onKeyDown);
170
+ window.addEventListener('keyup', onKeyUp);
171
+ canvas.addEventListener('pointerdown', onPointerDown);
172
+ canvas.addEventListener('pointermove', onPointerMove);
173
+ canvas.addEventListener('pointerup', onPointerUp);
174
+ canvas.addEventListener('pointercancel', onPointerUp);
175
+ if (document.hasFocus() || window.parent === window) canvas.focus();
176
+ return () => {
177
+ window.removeEventListener('keydown', onKeyDown);
178
+ window.removeEventListener('keyup', onKeyUp);
179
+ canvas.removeEventListener('pointerdown', onPointerDown);
180
+ canvas.removeEventListener('pointermove', onPointerMove);
181
+ canvas.removeEventListener('pointerup', onPointerUp);
182
+ canvas.removeEventListener('pointercancel', onPointerUp);
183
+ };
184
+ }
185
+
186
+ function attachResize(canvas, world) {
187
+ const fit = () => resizeWorld(world, canvas.clientWidth, canvas.clientHeight);
188
+ const observer = new ResizeObserver(fit);
189
+ observer.observe(canvas);
190
+ fit();
191
+ return () => observer.disconnect();
192
+ }
193
+
194
+ function startLoop(runtime, world, sprites, hudRefs, onFirstFrame, onError) {
195
+ let raf = 0;
196
+ let previousTime = performance.now();
197
+ let firstFrameSignaled = false;
198
+ const tick = (now) => {
199
+ const dt = Math.min(0.033, (now - previousTime) / 1000);
200
+ previousTime = now;
201
+ try {
202
+ runtime.update(dt);
203
+ // A Portal (or any behavior) may have requested a scene switch; apply
204
+ // it here, between frames, never mid physics dispatch. The load resets
205
+ // registered systems -- the rapier world frees and lazily rebuilds --
206
+ // while `runtime.persistent` carries the player's cross-scene state.
207
+ if (runtime.requestedScene) {
208
+ const next = runtime.requestedScene;
209
+ runtime.requestedScene = null;
210
+ runtime.loadFromFile(next);
211
+ runtime.currentScenePath = next;
212
+ runtime.persistent.portalGraceUntil = runtime.time + 1;
213
+ }
214
+ syncActors(world, runtime, sprites, dt);
215
+ if (world.debugLines.visible) updatePhysicsDebug(world, runtime.physics3d?.debugRender());
216
+ renderWorld(world);
217
+ updateHud(runtime, hudRefs);
218
+ } catch (err) {
219
+ onError?.(err);
220
+ return;
221
+ }
222
+ if (!firstFrameSignaled) {
223
+ firstFrameSignaled = true;
224
+ requestAnimationFrame(() => onFirstFrame?.());
225
+ }
226
+ raf = requestAnimationFrame(tick);
227
+ };
228
+ raf = requestAnimationFrame(tick);
229
+ return () => cancelAnimationFrame(raf);
230
+ }
231
+
232
+ // Behaviors write `scene.hud = { score, look }`; the loop pushes it into the
233
+ // overlay divs imperatively (no react re-render at frame rate).
234
+ function updateHud(runtime, hudRefs) {
235
+ const hud = runtime.hud ?? {};
236
+ const scoreText = hud.score ? String(hud.score) : '';
237
+ const lookText = hud.look ?? '';
238
+ if (hudRefs.score.current && hudRefs.score.current.textContent !== scoreText) {
239
+ hudRefs.score.current.textContent = scoreText;
240
+ }
241
+ if (hudRefs.look.current && hudRefs.look.current.textContent !== lookText) {
242
+ hudRefs.look.current.textContent = lookText;
243
+ }
244
+ }
245
+
246
+ const hudStyles = {
247
+ score: {
248
+ position: 'absolute',
249
+ top: 14,
250
+ left: 16,
251
+ color: '#7bd9ed',
252
+ font: "16px/1.4 'SFMono-Regular', Consolas, monospace",
253
+ textShadow: '0 1px 2px rgba(0,0,0,0.8)',
254
+ pointerEvents: 'none',
255
+ },
256
+ look: {
257
+ position: 'absolute',
258
+ bottom: 22,
259
+ left: 0,
260
+ right: 0,
261
+ textAlign: 'center',
262
+ color: '#e6e6e2',
263
+ fontStyle: 'italic',
264
+ font: 'italic 15px/1.4 Georgia, serif',
265
+ textShadow: '0 1px 3px rgba(0,0,0,0.9)',
266
+ pointerEvents: 'none',
267
+ },
268
+ };
269
+
270
+ function LoopErrorOverlay({ error, onRestart }) {
271
+ return (
272
+ <div
273
+ style={{
274
+ position: 'absolute',
275
+ inset: 0,
276
+ zIndex: 2147483000,
277
+ padding: '20px 22px',
278
+ background: 'rgba(20, 8, 8, 0.94)',
279
+ color: '#ffd9dc',
280
+ font: "13px/1.5 'SFMono-Regular', Consolas, monospace",
281
+ overflow: 'auto',
282
+ }}>
283
+ <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 12 }}>
284
+ <span style={{ fontSize: 15, fontWeight: 700, color: '#ff6b74' }}>Play error</span>
285
+ <button
286
+ type="button"
287
+ onClick={onRestart}
288
+ style={{
289
+ marginLeft: 'auto',
290
+ padding: '4px 12px',
291
+ border: '1px solid #a02828',
292
+ borderRadius: 4,
293
+ background: '#a02828',
294
+ color: '#fff',
295
+ font: 'inherit',
296
+ cursor: 'pointer',
297
+ }}>
298
+ Restart
299
+ </button>
300
+ </div>
301
+ <div style={{ color: '#fff', fontWeight: 600 }}>
302
+ {error?.name ?? 'Error'}: {error?.message ?? String(error)}
303
+ </div>
304
+ {typeof error?.stack === 'string' ? (
305
+ <pre style={{ whiteSpace: 'pre-wrap', color: '#e79aa0', fontSize: 12 }}>{error.stack}</pre>
306
+ ) : null}
307
+ </div>
308
+ );
309
+ }
@@ -0,0 +1,114 @@
1
+ // Data-side helpers for the 3d scene editor: the 3d counterparts of the
2
+ // imported engine's placement / duplication helpers (which write 2d Layout),
3
+ // plus the blueprint-file template-id shim. Everything here works on parsed
4
+ // scene JSON and returns fresh copies -- commits go through the editor's
5
+ // history.
6
+ import {
7
+ getBlueprintTemplate,
8
+ mintActorIdFromName,
9
+ mintBlueprintFile,
10
+ } from '@imports/castle.physics-2d/engine/blueprint';
11
+
12
+ export const TEMPLATE_ACTOR_ID = '__template__';
13
+
14
+ export const round2 = (v) => Math.round(v * 100) / 100;
15
+
16
+ // Grid snap for placements; matches the gizmo's 0.1m translation snap.
17
+ const snap = (v) => Math.round(v * 10) / 10;
18
+
19
+ // A blueprint file's template actor has no id on disk; inject a synthetic one
20
+ // so selection / inspector / gizmo code sees a normal actor, and strip it
21
+ // before every write.
22
+ export function withTemplateId(sceneData, isBlueprintFile) {
23
+ if (!sceneData || !isBlueprintFile) return sceneData;
24
+ const next = structuredClone(sceneData);
25
+ next.actors = next.actors ?? [];
26
+ if (next.actors[0]) next.actors[0].id = TEMPLATE_ACTOR_ID;
27
+ return next;
28
+ }
29
+
30
+ export function stripTemplateId(sceneData) {
31
+ const next = structuredClone(sceneData);
32
+ if (next.actors?.[0]?.id === TEMPLATE_ACTOR_ID) delete next.actors[0].id;
33
+ return next;
34
+ }
35
+
36
+ // Place a new instance of a blueprint at a ground point. Only Transform x/z
37
+ // are written on the instance -- height and everything else inherit.
38
+ export function placeBlueprintInstance3D(sceneData, files, blueprintPath, point) {
39
+ const next = structuredClone(sceneData);
40
+ next.actors = next.actors ?? [];
41
+ const existingIds = new Set(next.actors.map((actor) => actor.id));
42
+ const template = getBlueprintTemplate(files, blueprintPath);
43
+ const newId = mintActorIdFromName(existingIds, template?.name);
44
+ next.actors.push({
45
+ id: newId,
46
+ blueprint: blueprintPath,
47
+ components: { Transform: { x: snap(point.x), z: snap(point.z) } },
48
+ });
49
+ return { sceneData: next, newId };
50
+ }
51
+
52
+ // Duplicate selected actors with a 1m diagonal offset (the 3d version of the
53
+ // imported engine's duplicateActors, which offsets 2d Layout instead).
54
+ // `resolvedActors` supplies merged Transforms so an instance that inherits its
55
+ // position still lands next to the original.
56
+ export function duplicateActors3D(sceneData, actorIds, resolvedActors) {
57
+ const next = structuredClone(sceneData);
58
+ const existingIds = new Set(next.actors.map((actor) => actor.id));
59
+ const newIds = [];
60
+ for (const id of actorIds) {
61
+ const source = next.actors.find((actor) => actor.id === id);
62
+ if (!source) continue;
63
+ const copy = structuredClone(source);
64
+ copy.id = mintActorIdFromName(existingIds, id.replace(/-\d+$/, ''));
65
+ existingIds.add(copy.id);
66
+ const merged = resolvedActors?.find((actor) => actor.id === id)?.components?.Transform ?? {};
67
+ copy.components = {
68
+ ...(copy.components ?? {}),
69
+ Transform: {
70
+ ...(copy.components?.Transform ?? {}),
71
+ x: round2((merged.x ?? 0) + 1),
72
+ z: round2((merged.z ?? 0) + 1),
73
+ },
74
+ };
75
+ next.actors.push(copy);
76
+ newIds.push(copy.id);
77
+ }
78
+ return { sceneData: next, newIds };
79
+ }
80
+
81
+ // The hotbar's "+" slot: mint a fresh 3d blueprint file (a solid box) and
82
+ // place one instance of it at the origin.
83
+ export function addActorWithBlueprint3D(sceneData, files) {
84
+ const blueprintFile = mintBlueprintFile(files, {
85
+ Transform: { y: 0.5 },
86
+ Shape: { kind: 'box' },
87
+ Solid: {},
88
+ });
89
+ const filesWithNew = { ...files, [blueprintFile.path]: blueprintFile.text };
90
+ const { sceneData: next, newId } = placeBlueprintInstance3D(
91
+ sceneData,
92
+ filesWithNew,
93
+ blueprintFile.path,
94
+ { x: 0, z: 0 }
95
+ );
96
+ return { sceneData: next, newId, blueprintFile };
97
+ }
98
+
99
+ // A gizmo gesture's end state -> sparse Transform override patch. Position is
100
+ // always written (instance-local); rotation only when meaningful or already
101
+ // present.
102
+ export function transformPatch(existing, change) {
103
+ const next = {
104
+ ...(existing ?? {}),
105
+ x: round2(change.x),
106
+ y: round2(change.y),
107
+ z: round2(change.z),
108
+ };
109
+ for (const key of ['rotationX', 'rotationY', 'rotationZ']) {
110
+ const value = Math.round((change[key] ?? 0) * 10) / 10;
111
+ if (value !== 0 || (existing && existing[key] !== undefined)) next[key] = value;
112
+ }
113
+ return next;
114
+ }