castle-web-cli 0.4.129 → 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 (144) 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 +9 -10
  16. package/kits/physics-2d/castle.json +1 -1
  17. package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
  18. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  19. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  20. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  21. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  22. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  23. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  24. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  25. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  26. package/kits/physics-2d/engine/blueprint.js +15 -3
  27. package/kits/physics-2d/engine/liveReload.js +33 -21
  28. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  29. package/kits/physics-2d/engine/ui.jsx +2 -2
  30. package/kits/physics-2d/engine/ui.module.css +42 -0
  31. package/kits/physics-3d/CLAUDE.md +109 -0
  32. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  33. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  34. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  35. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  36. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  37. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  38. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  39. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  40. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  41. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  42. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  43. package/kits/physics-3d/blueprints/crate.scene +29 -0
  44. package/kits/physics-3d/blueprints/door.scene +35 -0
  45. package/kits/physics-3d/blueprints/gem.scene +26 -0
  46. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  47. package/kits/physics-3d/blueprints/platform.scene +23 -0
  48. package/kits/physics-3d/blueprints/player.scene +29 -0
  49. package/kits/physics-3d/blueprints/portal.scene +21 -0
  50. package/kits/physics-3d/blueprints/rock.scene +14 -0
  51. package/kits/physics-3d/blueprints/statue.scene +26 -0
  52. package/kits/physics-3d/blueprints/tree.scene +14 -0
  53. package/kits/physics-3d/blueprints/wall.scene +26 -0
  54. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  55. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  56. package/kits/physics-3d/drawings/crate.pxart +26 -0
  57. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  58. package/kits/physics-3d/drawings/door.pxart +27 -0
  59. package/kits/physics-3d/drawings/face.pxart +26 -0
  60. package/kits/physics-3d/drawings/floor.pxart +27 -0
  61. package/kits/physics-3d/drawings/platform.pxart +27 -0
  62. package/kits/physics-3d/drawings/statue.pxart +27 -0
  63. package/kits/physics-3d/drawings/wall.pxart +27 -0
  64. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
  65. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  66. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  67. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  68. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  69. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  70. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  71. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  72. package/kits/physics-3d/engine3d/materials.js +174 -0
  73. package/kits/physics-3d/engine3d/meshops.js +123 -0
  74. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  75. package/kits/physics-3d/engine3d/modelFiles.js +139 -0
  76. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  77. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  78. package/kits/physics-3d/engine3d/world3d.js +216 -0
  79. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  80. package/kits/physics-3d/index.html +20 -0
  81. package/kits/physics-3d/main.jsx +30 -0
  82. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  83. package/kits/physics-3d/models/player.pxmodel +222 -0
  84. package/kits/physics-3d/models/rock.pxmodel +30 -0
  85. package/kits/physics-3d/models/tree.pxmodel +10 -0
  86. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  87. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  88. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  89. package/kits/physics-3d/scenes/main.scene +414 -0
  90. package/kits/physics-3d/scenes/model-lab.scene +127 -0
  91. package/kits/physics-3d/systems/physics3d.js +356 -0
  92. package/package.json +5 -2
  93. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  94. package/kits/basic-2d/CLAUDE.md +0 -221
  95. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  96. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  97. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  98. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  99. package/kits/basic-2d/behaviors/tint.js +0 -47
  100. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  101. package/kits/basic-2d/docs/pxart-format.md +0 -377
  102. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  103. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  104. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  105. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  106. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  107. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  108. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  109. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  110. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  111. package/kits/basic-2d/editors/editorHistory.js +0 -157
  112. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  113. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  114. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  115. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  116. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  117. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  118. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  119. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  120. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  121. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  122. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  123. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  124. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  125. package/kits/basic-2d/engine/assets.js +0 -15
  126. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  127. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  128. package/kits/basic-2d/engine/blueprint.js +0 -557
  129. package/kits/basic-2d/engine/collider.js +0 -200
  130. package/kits/basic-2d/engine/files.js +0 -141
  131. package/kits/basic-2d/engine/liveReload.js +0 -88
  132. package/kits/basic-2d/engine/pxart.js +0 -1032
  133. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  134. package/kits/basic-2d/engine/scene.js +0 -696
  135. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  136. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  137. package/kits/basic-2d/engine/ui.jsx +0 -695
  138. package/kits/basic-2d/engine/ui.module.css +0 -2287
  139. package/kits/basic-2d/index.html +0 -24
  140. package/kits/basic-2d/main.jsx +0 -24
  141. package/kits/basic-2d/scenes/main.scene +0 -16
  142. package/kits/basic-2d/scripts/draw.mjs +0 -121
  143. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  144. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -0,0 +1,439 @@
1
+ // The three.js side of the .pxmodel editor: one model on the world3d stage,
2
+ // orbit camera, part picking, a selection box on the picked part, and
3
+ // move/rotate gizmos that write back part-local placement. The react chrome
4
+ // (PxModelEditor.jsx) owns the file text, part list, and saving.
5
+ import * as THREE from 'three';
6
+ import { OrbitControls, TransformControls } from 'three/examples/jsm/Addons.js';
7
+ import { makeWorld3D, resizeWorld, renderWorld, disposeWorld } from './world3d';
8
+ import { buildModelNode } from './pxmodel';
9
+ import { buildMeshGeometry, faceVertexIndices } from './meshops';
10
+ import { disposeNode } from './materials';
11
+
12
+ const CLICK_SLOP = 5;
13
+
14
+ export function createModelEditorWorld(canvas, callbacks) {
15
+ const world = makeWorld3D(canvas);
16
+ world.camera.position.set(3, 2.4, 3);
17
+ const orbit = new OrbitControls(world.camera, canvas);
18
+ orbit.enableDamping = true;
19
+ orbit.dampingFactor = 0.12;
20
+ orbit.target.set(0, 0.7, 0);
21
+ const gizmo = new TransformControls(world.camera, canvas);
22
+ gizmo.setTranslationSnap(0.01);
23
+ gizmo.setRotationSnap(THREE.MathUtils.degToRad(5));
24
+ world.scene.add(gizmo.getHelper());
25
+
26
+ const med = {
27
+ world,
28
+ orbit,
29
+ gizmo,
30
+ canvas,
31
+ callbacks,
32
+ modelNode: null,
33
+ partGroups: null,
34
+ mode: 'select',
35
+ selectedPartId: null,
36
+ gizmoDragging: false,
37
+ walk: false,
38
+ walkClock: 0,
39
+ box: null,
40
+ raf: 0,
41
+ model: null,
42
+ edit: null, // { partId, mode: 'face'|'vertex', selection: [indices] }
43
+ editVis: null,
44
+ editProxy: null,
45
+ editDrag: null,
46
+ };
47
+
48
+ gizmo.addEventListener('dragging-changed', (event) => {
49
+ if (!event.value) med.gizmoDraggedUntil = performance.now() + 150;
50
+ med.gizmoDragging = event.value;
51
+ med.orbit.enabled = !event.value;
52
+ if (med.edit) {
53
+ if (event.value) beginEditDrag(med);
54
+ else endEditDrag(med);
55
+ } else if (!event.value) {
56
+ commitGizmo(med);
57
+ }
58
+ });
59
+ gizmo.addEventListener('objectChange', () => {
60
+ if (med.edit && med.gizmoDragging) applyEditDrag(med);
61
+ });
62
+
63
+ let downAt = null;
64
+ canvas.addEventListener('pointerdown', (event) => {
65
+ downAt = { x: event.clientX, y: event.clientY };
66
+ });
67
+ canvas.addEventListener('pointerup', (event) => {
68
+ if (!downAt) return;
69
+ const travel = Math.hypot(event.clientX - downAt.x, event.clientY - downAt.y);
70
+ downAt = null;
71
+ if (travel > CLICK_SLOP || med.gizmoDragging) return;
72
+ if (performance.now() < (med.gizmoDraggedUntil ?? 0)) return;
73
+ if (med.edit) med.callbacks.onEditPick(pickMeshElement(med, event), event.shiftKey);
74
+ else med.callbacks.onPickPart(pickPart(med, event));
75
+ });
76
+
77
+ startModelLoop(med);
78
+ return med;
79
+ }
80
+
81
+ export function setEditorModel(med, model, sprites) {
82
+ clearEditVisuals(med);
83
+ if (med.modelNode) {
84
+ med.gizmo.detach();
85
+ med.world.scene.remove(med.modelNode);
86
+ disposeNode(med.modelNode);
87
+ }
88
+ // A private clone: edit-mode drags mutate this copy live (the parse cache
89
+ // shares model objects with the play runtime, so never mutate those).
90
+ med.model = model ? structuredClone(model) : null;
91
+ med.modelNode = med.model ? buildModelNode(med.model, sprites) : null;
92
+ med.partGroups = med.modelNode?.userData.modelParts ?? null;
93
+ if (med.modelNode) med.world.scene.add(med.modelNode);
94
+ }
95
+
96
+ export function disposeModelEditorWorld(med) {
97
+ cancelAnimationFrame(med.raf);
98
+ med.resizeObserver?.disconnect();
99
+ clearSelectionBox(med);
100
+ med.gizmo.detach();
101
+ med.gizmo.dispose();
102
+ med.orbit.dispose();
103
+ if (med.modelNode) disposeNode(med.modelNode);
104
+ disposeWorld(med.world);
105
+ }
106
+
107
+ function screenRaycaster(med, event) {
108
+ const rect = med.canvas.getBoundingClientRect();
109
+ const ndc = new THREE.Vector2(
110
+ ((event.clientX - rect.left) / rect.width) * 2 - 1,
111
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
112
+ );
113
+ const raycaster = new THREE.Raycaster();
114
+ raycaster.setFromCamera(ndc, med.world.camera);
115
+ return raycaster;
116
+ }
117
+
118
+ function pickPart(med, event) {
119
+ if (!med.modelNode) return null;
120
+ const hits = screenRaycaster(med, event).intersectObject(med.modelNode, true);
121
+ for (const hit of hits) {
122
+ let object = hit.object;
123
+ while (object && object.userData.partId === undefined) object = object.parent;
124
+ if (object?.userData.partId) return object.userData.partId;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ function commitGizmo(med) {
130
+ const group = med.gizmo.object;
131
+ if (!group || !group.userData.partId) return;
132
+ const r = (v) => Math.round(v * 100) / 100;
133
+ const deg = (v) => Math.round(THREE.MathUtils.radToDeg(v) * 10) / 10;
134
+ med.callbacks.onCommitPart(group.userData.partId, {
135
+ position: [r(group.position.x), r(group.position.y), r(group.position.z)],
136
+ rotation: [deg(group.rotation.x), deg(group.rotation.y), deg(group.rotation.z)],
137
+ });
138
+ }
139
+
140
+ function clearSelectionBox(med) {
141
+ if (!med.box) return;
142
+ med.world.scene.remove(med.box.helper);
143
+ med.box.helper.dispose();
144
+ med.box = null;
145
+ }
146
+
147
+ function syncSelection(med) {
148
+ if (med.edit) {
149
+ clearSelectionBox(med);
150
+ return;
151
+ }
152
+ const group = med.selectedPartId ? (med.partGroups?.get(med.selectedPartId) ?? null) : null;
153
+ if (!group || med.box?.group !== group) clearSelectionBox(med);
154
+ if (group && !med.box) {
155
+ med.box = { helper: new THREE.BoxHelper(group, 0xffffff), group };
156
+ med.world.scene.add(med.box.helper);
157
+ }
158
+ med.box?.helper.update();
159
+ const wantGizmo = group && (med.mode === 'move' || med.mode === 'rotate');
160
+ if (wantGizmo) {
161
+ med.gizmo.mode = med.mode === 'move' ? 'translate' : 'rotate';
162
+ if (med.gizmo.object !== group) med.gizmo.attach(group);
163
+ } else if (med.gizmo.object) {
164
+ med.gizmo.detach();
165
+ }
166
+ }
167
+
168
+ // A canned stride so the walk toggle can preview the animation conventions
169
+ // without anything actually moving.
170
+ function previewWalk(med, dt) {
171
+ if (!med.walk || !med.partGroups) return;
172
+ med.walkClock += dt;
173
+ const swing = Math.sin(med.walkClock * 7) * 0.6;
174
+ const set = (id, rx) => {
175
+ const group = med.partGroups.get(id);
176
+ if (group) group.rotation.x = rx;
177
+ };
178
+ set('legL', swing);
179
+ set('legR', -swing);
180
+ set('armL', -swing * 0.75);
181
+ set('armR', swing * 0.75);
182
+ const torso = med.partGroups.get('torso');
183
+ if (torso) {
184
+ const base = (torso.userData.editBaseY ??= torso.position.y);
185
+ torso.position.y = base + Math.abs(Math.sin(med.walkClock * 7)) * 0.05;
186
+ }
187
+ }
188
+
189
+ function startModelLoop(med) {
190
+ const fit = () => resizeWorld(med.world, med.canvas.clientWidth, med.canvas.clientHeight);
191
+ med.resizeObserver = new ResizeObserver(fit);
192
+ med.resizeObserver.observe(med.canvas);
193
+ fit();
194
+ let previous = performance.now();
195
+ const tick = (now) => {
196
+ const dt = Math.min(0.033, (now - previous) / 1000);
197
+ previous = now;
198
+ med.orbit.update();
199
+ previewWalk(med, dt);
200
+ syncSelection(med);
201
+ syncMeshEdit(med);
202
+ renderWorld(med.world);
203
+ med.raf = requestAnimationFrame(tick);
204
+ };
205
+ med.raf = requestAnimationFrame(tick);
206
+ }
207
+
208
+ //
209
+ // Mesh edit mode
210
+ //
211
+
212
+ function editContext(med) {
213
+ const edit = med.edit;
214
+ const part = edit ? med.model?.parts.find((candidate) => candidate.id === edit.partId) : null;
215
+ const group = edit ? (med.partGroups?.get(edit.partId) ?? null) : null;
216
+ const meshObj = group?.children.find((child) => child.isMesh) ?? null;
217
+ if (!edit || !part || part.kind !== 'mesh' || !group || !meshObj) return null;
218
+ return { edit, part, group, meshObj };
219
+ }
220
+
221
+ function pickMeshElement(med, event) {
222
+ const ctx = editContext(med);
223
+ if (!ctx) return null;
224
+ const rect = med.canvas.getBoundingClientRect();
225
+ if (ctx.edit.mode === 'face') {
226
+ const hit = screenRaycaster(med, event).intersectObject(ctx.meshObj, false)[0];
227
+ if (!hit || hit.faceIndex === undefined) return null;
228
+ return ctx.meshObj.userData.triFaces?.[hit.faceIndex] ?? null;
229
+ }
230
+ // Vertex mode: nearest projected vertex within a small screen radius.
231
+ const world = new THREE.Vector3();
232
+ let best = null;
233
+ let bestDist = 14;
234
+ for (let index = 0; index < ctx.part.vertices.length; index++) {
235
+ const v = ctx.part.vertices[index];
236
+ world
237
+ .set(v[0] + ctx.part.offset[0], v[1] + ctx.part.offset[1], v[2] + ctx.part.offset[2])
238
+ .applyMatrix4(ctx.group.matrixWorld)
239
+ .project(med.world.camera);
240
+ const sx = ((world.x + 1) / 2) * rect.width;
241
+ const sy = ((1 - world.y) / 2) * rect.height;
242
+ const dist = Math.hypot(sx - (event.clientX - rect.left), sy - (event.clientY - rect.top));
243
+ if (dist < bestDist) {
244
+ bestDist = dist;
245
+ best = index;
246
+ }
247
+ }
248
+ return best;
249
+ }
250
+
251
+ function selectedVertexIndices(ctx) {
252
+ return ctx.edit.mode === 'face'
253
+ ? faceVertexIndices(ctx.part, ctx.edit.selection)
254
+ : ctx.edit.selection;
255
+ }
256
+
257
+ function beginEditDrag(med) {
258
+ const ctx = editContext(med);
259
+ if (!ctx || !med.editProxy) return;
260
+ med.editDrag = {
261
+ start: med.editProxy.position.clone(),
262
+ vertices: structuredClone(ctx.part.vertices),
263
+ };
264
+ }
265
+
266
+ function applyEditDrag(med) {
267
+ const ctx = editContext(med);
268
+ if (!ctx || !med.editDrag || !med.editProxy) return;
269
+ const delta = med.editProxy.position.clone().sub(med.editDrag.start);
270
+ for (const index of selectedVertexIndices(ctx)) {
271
+ const v = med.editDrag.vertices[index];
272
+ if (v) ctx.part.vertices[index] = [v[0] + delta.x, v[1] + delta.y, v[2] + delta.z];
273
+ }
274
+ rebuildEditGeometry(med, ctx);
275
+ }
276
+
277
+ function endEditDrag(med) {
278
+ const ctx = editContext(med);
279
+ const drag = med.editDrag;
280
+ med.editDrag = null;
281
+ if (!ctx || !drag) return;
282
+ const round3 = (v) => Math.round(v * 1000) / 1000;
283
+ med.callbacks.onCommitMeshVertices(
284
+ ctx.edit.partId,
285
+ ctx.part.vertices.map((v) => v.map(round3))
286
+ );
287
+ }
288
+
289
+ function rebuildEditGeometry(med, ctx) {
290
+ const { geometry, triFaces } = buildMeshGeometry(ctx.part);
291
+ ctx.meshObj.geometry.dispose();
292
+ ctx.meshObj.geometry = geometry;
293
+ ctx.meshObj.userData.triFaces = triFaces;
294
+ refreshEditVisuals(med, ctx, true);
295
+ }
296
+
297
+ function disposeEditVisObjects(med) {
298
+ if (!med.editVis) return;
299
+ for (const object of Object.values(med.editVis.objects)) {
300
+ object.parent?.remove(object);
301
+ object.geometry?.dispose();
302
+ object.material?.dispose();
303
+ }
304
+ med.editVis = null;
305
+ }
306
+
307
+ function clearEditVisuals(med) {
308
+ disposeEditVisObjects(med);
309
+ if (med.editProxy) {
310
+ if (med.gizmo.object === med.editProxy) med.gizmo.detach();
311
+ med.editProxy.parent?.remove(med.editProxy);
312
+ med.editProxy = null;
313
+ }
314
+ }
315
+
316
+ function editVisSignature(ctx) {
317
+ return JSON.stringify([ctx.edit.mode, ctx.edit.selection, ctx.part.vertices.length]);
318
+ }
319
+
320
+ // Per-frame edit overlays: vertex dots, selected-face tint, and the translate
321
+ // proxy the gizmo drives. Rebuilt only when the selection/mesh changes.
322
+ function syncMeshEdit(med) {
323
+ const ctx = editContext(med);
324
+ if (!ctx) {
325
+ clearEditVisuals(med);
326
+ return;
327
+ }
328
+ const sig = editVisSignature(ctx);
329
+ if (med.editVis?.sig !== sig && !med.gizmoDragging) {
330
+ clearEditVisuals(med);
331
+ med.editVis = { sig, objects: buildEditVisuals(med, ctx) };
332
+ syncEditProxy(med, ctx);
333
+ }
334
+ const wantGizmo = (med.mode === 'move' || med.mode === 'rotate') && ctx.edit.selection.length > 0;
335
+ if (wantGizmo && med.editProxy) {
336
+ med.gizmo.mode = 'translate';
337
+ if (med.gizmo.object !== med.editProxy) med.gizmo.attach(med.editProxy);
338
+ } else if (med.gizmo.object && !med.gizmoDragging) {
339
+ med.gizmo.detach();
340
+ }
341
+ }
342
+
343
+ function refreshEditVisuals(med, ctx, keepProxy) {
344
+ disposeEditVisObjects(med);
345
+ med.editVis = { sig: editVisSignature(ctx), objects: buildEditVisuals(med, ctx) };
346
+ if (!keepProxy) syncEditProxy(med, ctx);
347
+ }
348
+
349
+ function syncEditProxy(med, ctx) {
350
+ if (!med.editProxy) {
351
+ med.editProxy = new THREE.Object3D();
352
+ ctx.group.add(med.editProxy);
353
+ } else if (med.editProxy.parent !== ctx.group) {
354
+ med.editProxy.parent?.remove(med.editProxy);
355
+ ctx.group.add(med.editProxy);
356
+ }
357
+ const indices = selectedVertexIndices(ctx);
358
+ if (indices.length === 0) return;
359
+ const c = [0, 0, 0];
360
+ for (const index of indices) {
361
+ const v = ctx.part.vertices[index] ?? [0, 0, 0];
362
+ c[0] += v[0];
363
+ c[1] += v[1];
364
+ c[2] += v[2];
365
+ }
366
+ med.editProxy.position.set(
367
+ c[0] / indices.length + ctx.part.offset[0],
368
+ c[1] / indices.length + ctx.part.offset[1],
369
+ c[2] / indices.length + ctx.part.offset[2]
370
+ );
371
+ }
372
+
373
+ function buildEditVisuals(med, ctx) {
374
+ const objects = {};
375
+ const offset = ctx.part.offset;
376
+ const positions = [];
377
+ for (const v of ctx.part.vertices) {
378
+ positions.push(v[0] + offset[0], v[1] + offset[1], v[2] + offset[2]);
379
+ }
380
+ const allGeometry = new THREE.BufferGeometry();
381
+ allGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
382
+ objects.points = new THREE.Points(
383
+ allGeometry,
384
+ new THREE.PointsMaterial({ color: 0x8a92a8, size: 6, sizeAttenuation: false, depthTest: false })
385
+ );
386
+ objects.points.renderOrder = 10;
387
+ ctx.group.add(objects.points);
388
+
389
+ const selectedVerts = selectedVertexIndices(ctx);
390
+ if (selectedVerts.length) {
391
+ const selPositions = [];
392
+ for (const index of selectedVerts) {
393
+ const v = ctx.part.vertices[index] ?? [0, 0, 0];
394
+ selPositions.push(v[0] + offset[0], v[1] + offset[1], v[2] + offset[2]);
395
+ }
396
+ const selGeometry = new THREE.BufferGeometry();
397
+ selGeometry.setAttribute('position', new THREE.Float32BufferAttribute(selPositions, 3));
398
+ objects.selectedPoints = new THREE.Points(
399
+ selGeometry,
400
+ new THREE.PointsMaterial({
401
+ color: 0xffffff,
402
+ size: 10,
403
+ sizeAttenuation: false,
404
+ depthTest: false,
405
+ })
406
+ );
407
+ objects.selectedPoints.renderOrder = 11;
408
+ ctx.group.add(objects.selectedPoints);
409
+ }
410
+
411
+ if (ctx.edit.mode === 'face' && ctx.edit.selection.length) {
412
+ const facePositions = [];
413
+ for (const faceIndex of ctx.edit.selection) {
414
+ const face = ctx.part.faces[faceIndex];
415
+ if (!face) continue;
416
+ for (let i = 1; i + 1 < face.length; i++) {
417
+ for (const index of [face[0], face[i], face[i + 1]]) {
418
+ const v = ctx.part.vertices[index] ?? [0, 0, 0];
419
+ facePositions.push(v[0] + offset[0], v[1] + offset[1], v[2] + offset[2]);
420
+ }
421
+ }
422
+ }
423
+ const faceGeometry = new THREE.BufferGeometry();
424
+ faceGeometry.setAttribute('position', new THREE.Float32BufferAttribute(facePositions, 3));
425
+ objects.faceOverlay = new THREE.Mesh(
426
+ faceGeometry,
427
+ new THREE.MeshBasicMaterial({
428
+ color: 0xffe066,
429
+ transparent: true,
430
+ opacity: 0.3,
431
+ side: THREE.DoubleSide,
432
+ depthTest: false,
433
+ })
434
+ );
435
+ objects.faceOverlay.renderOrder = 9;
436
+ ctx.group.add(objects.faceOverlay);
437
+ }
438
+ return objects;
439
+ }
@@ -0,0 +1,139 @@
1
+ // `.pxmodel` files in the live deck file map. The imported engine's file
2
+ // globs are compile-time literals that don't know this kit's model format, so
3
+ // this module (a) adds its own glob so models are seeded at build time and
4
+ // inlined into the published bundle, and (b) forks the imported
5
+ // useLiveDeckFiles with `.pxmodel` treated as a DATA file -- edited live over
6
+ // the files API instead of forcing a context reload (the glob puts models in
7
+ // vite's module graph, which the imported hook would read as "code changed").
8
+ import { useEffect, useRef, useState } from 'react';
9
+ import { onFilesChanged, requestReload } from 'castle-web-sdk';
10
+ import { initialFiles, initialMedia, isImageFile } from '@imports/castle.physics-2d/engine/files';
11
+ import { parseFull } from '@imports/castle.physics-2d/engine/pxart';
12
+ import { imageArt } from '@imports/castle.physics-2d/engine/art';
13
+
14
+ // Both the deck's own models and every import's -- same shape as the imported
15
+ // engine's own file globs: when this kit is itself an import, its models live
16
+ // under imports/<alias>/models/ in the deck being served, and a map without
17
+ // them can't resolve a kit blueprint's Model ref.
18
+ const modelModules = import.meta.glob(['/models/**/*.pxmodel', '/imports/*/models/**/*.pxmodel'], {
19
+ query: '?raw',
20
+ import: 'default',
21
+ eager: true,
22
+ });
23
+
24
+ export const initialFiles3D = {
25
+ ...initialFiles,
26
+ ...Object.fromEntries(
27
+ Object.entries(modelModules).map(([globPath, text]) => [globPath.replace(/^\//, ''), text])
28
+ ),
29
+ };
30
+
31
+ const DATA_EXTS = ['.scene', '.pxart', '.drawing', '.pxmodel'];
32
+ const CODE_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.css'];
33
+
34
+ export function isDataFile3D(path) {
35
+ return DATA_EXTS.some((ext) => path.endsWith(ext));
36
+ }
37
+
38
+ function needsContextReload(change) {
39
+ if (isDataFile3D(change.path)) return false;
40
+ if (change.path.endsWith('.html')) return true;
41
+ if (change.affected.length > 0) return true;
42
+ if (change.event !== 'change') return CODE_EXTS.some((ext) => change.path.endsWith(ext));
43
+ return false;
44
+ }
45
+
46
+ async function readDeckFile(path) {
47
+ const res = await fetch(`/__castle/files/read?path=${encodeURIComponent(path)}`);
48
+ if (!res.ok) throw new Error(`read failed: ${res.status}`);
49
+ const data = await res.json();
50
+ return typeof data.contents === 'string' ? data.contents : '';
51
+ }
52
+
53
+ async function applyDataChanges(changes, setFiles, setDataVersion) {
54
+ const updates = await Promise.all(
55
+ changes.map(async (change) => {
56
+ if (change.event === 'delete') return { path: change.path, text: null };
57
+ try {
58
+ return { path: change.path, text: await readDeckFile(change.path) };
59
+ } catch {
60
+ return null; // read raced a delete / serve hiccup -- skip this path
61
+ }
62
+ })
63
+ );
64
+ const applied = updates.filter(Boolean);
65
+ if (applied.length === 0) return;
66
+ setFiles((current) => {
67
+ const next = { ...current };
68
+ for (const { path, text } of applied) {
69
+ if (text === null) delete next[path];
70
+ else next[path] = text;
71
+ }
72
+ return next;
73
+ });
74
+ setDataVersion((version) => version + 1);
75
+ }
76
+
77
+ // Live deck files including models. Same contract as the imported
78
+ // useLiveDeckFiles: `shouldSkipPath` keeps an editor's in-flight writes from
79
+ // being clobbered by their own fs echo; `dataVersion` bumps on applied data
80
+ // changes.
81
+ export function useLiveDeckFiles3D({ shouldSkipPath } = {}) {
82
+ const [files, setFiles] = useState(initialFiles3D);
83
+ const [dataVersion, setDataVersion] = useState(0);
84
+ const skipRef = useRef(shouldSkipPath);
85
+ skipRef.current = shouldSkipPath;
86
+
87
+ useEffect(
88
+ () =>
89
+ onFilesChanged((event) => {
90
+ if (event.changes.some(needsContextReload)) {
91
+ requestReload();
92
+ return;
93
+ }
94
+ const dataChanges = event.changes.filter(
95
+ (change) => isDataFile3D(change.path) && !skipRef.current?.(change.path)
96
+ );
97
+ if (dataChanges.length === 0) return;
98
+ void applyDataChanges(dataChanges, setFiles, setDataVersion);
99
+ }),
100
+ []
101
+ );
102
+
103
+ return { files, setFiles, dataVersion };
104
+ }
105
+
106
+ // The sprites map with parsed pxarts CACHED BY TEXT: re-parsing every file on
107
+ // every edit would mint new art objects, changing every node signature and
108
+ // rebuilding the whole three scene per keystroke. Only a pxart whose text
109
+ // actually changed re-parses. Shared by the scene and model editors.
110
+ export function useStableSprites(files) {
111
+ const cacheRef = useRef(new Map());
112
+ const sprites = {};
113
+ for (const [path, url] of Object.entries(initialMedia)) {
114
+ if (isImageFile(path)) sprites[path] = imageArt(path, url);
115
+ }
116
+ for (const [path, text] of Object.entries(files)) {
117
+ if (!path.endsWith('.pxart')) continue;
118
+ let cached = cacheRef.current.get(path);
119
+ if (!cached || cached.text !== text) {
120
+ const art = parseFull(text);
121
+ if (!art) continue;
122
+ cached = { text, art };
123
+ cacheRef.current.set(path, cached);
124
+ }
125
+ sprites[path] = cached.art;
126
+ }
127
+ return sprites;
128
+ }
129
+
130
+ // One-shot fetch for a path the build-time glob missed (a model created after
131
+ // the last reload); folds it into files state.
132
+ export async function fetchMissingFile(path, setFiles) {
133
+ try {
134
+ const text = await readDeckFile(path);
135
+ setFiles((current) => (current[path] === undefined ? { ...current, [path]: text } : current));
136
+ } catch {
137
+ // stays missing; the editor shows its error state
138
+ }
139
+ }