castle-web-cli 0.4.129 → 0.4.131
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.
- package/dist/agent-prompts.js +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +9 -10
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +16 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +10 -10
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/ScenePlayer.jsx +2 -2
- package/kits/physics-2d/engine/blueprint.js +23 -5
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/scene.js +17 -5
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- package/kits/physics-3d/CLAUDE.md +108 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +36 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +136 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +398 -0
- package/kits/physics-3d/scenes/model-lab.scene +111 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// The three.js side of the 3d scene editor: an orbit camera over the same
|
|
2
|
+
// world3d renderer the player uses, raycast picking with click-to-cycle,
|
|
3
|
+
// white selection boxes, and vase-style TransformControls gizmos for move /
|
|
4
|
+
// rotate (multi-select moves as a group). Pure imperative core -- the react
|
|
5
|
+
// chrome in Scene3DEditor.jsx owns file text, selection state, and saving,
|
|
6
|
+
// and talks to this through `ed.callbacks`.
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import { OrbitControls, TransformControls } from 'three/examples/jsm/Addons.js';
|
|
9
|
+
import { makeWorld3D, resizeWorld, syncActors, renderWorld, disposeWorld } from './world3d';
|
|
10
|
+
import { makeScene } from '@imports/castle.physics-2d/engine/scene';
|
|
11
|
+
|
|
12
|
+
const CLICK_SLOP_PX = 5;
|
|
13
|
+
|
|
14
|
+
export function createEditorWorld(canvas, callbacks) {
|
|
15
|
+
const world = makeWorld3D(canvas);
|
|
16
|
+
world.camera.position.set(11, 10, 11);
|
|
17
|
+
|
|
18
|
+
const orbit = new OrbitControls(world.camera, canvas);
|
|
19
|
+
orbit.enableDamping = true;
|
|
20
|
+
orbit.dampingFactor = 0.12;
|
|
21
|
+
orbit.target.set(0, 0.5, 0);
|
|
22
|
+
|
|
23
|
+
const gizmo = new TransformControls(world.camera, canvas);
|
|
24
|
+
gizmo.setTranslationSnap(0.1);
|
|
25
|
+
gizmo.setRotationSnap(THREE.MathUtils.degToRad(15));
|
|
26
|
+
world.scene.add(gizmo.getHelper());
|
|
27
|
+
|
|
28
|
+
const ed = {
|
|
29
|
+
world,
|
|
30
|
+
orbit,
|
|
31
|
+
gizmo,
|
|
32
|
+
canvas,
|
|
33
|
+
callbacks,
|
|
34
|
+
runtime: null,
|
|
35
|
+
sprites: {},
|
|
36
|
+
mode: 'select',
|
|
37
|
+
selection: [],
|
|
38
|
+
boxes: new Map(), // actorId -> THREE.BoxHelper
|
|
39
|
+
gizmoDragging: false,
|
|
40
|
+
dragStart: null,
|
|
41
|
+
raf: 0,
|
|
42
|
+
lastAppliedText: null,
|
|
43
|
+
};
|
|
44
|
+
wireGizmo(ed);
|
|
45
|
+
wirePointer(ed);
|
|
46
|
+
startEditorLoop(ed);
|
|
47
|
+
return ed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Swap in fresh scene data (parsed file JSON). The display runtime is never
|
|
51
|
+
// stepped -- actors sit at their authored Transforms; blueprints and default
|
|
52
|
+
// props merge exactly as in play.
|
|
53
|
+
export function setEditorScene(ed, sceneData, behaviors, sprites, files, scenePath) {
|
|
54
|
+
ed.runtime?.dispose();
|
|
55
|
+
ed.runtime = makeScene(sceneData, behaviors, sprites, files, scenePath);
|
|
56
|
+
ed.sprites = sprites;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function disposeEditorWorld(ed) {
|
|
60
|
+
cancelAnimationFrame(ed.raf);
|
|
61
|
+
ed.resizeObserver?.disconnect();
|
|
62
|
+
for (const entry of ed.boxes.values()) ed.world.scene.remove(entry.helper);
|
|
63
|
+
ed.boxes.clear();
|
|
64
|
+
ed.gizmo.detach();
|
|
65
|
+
ed.gizmo.dispose();
|
|
66
|
+
ed.orbit.dispose();
|
|
67
|
+
ed.runtime?.dispose();
|
|
68
|
+
disposeWorld(ed.world);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Merged Transform of a selected actor in the display runtime (degrees, live).
|
|
72
|
+
export function editorActorTransform(ed, actorId) {
|
|
73
|
+
return ed.runtime?.getActor(actorId)?.components?.Transform ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Ground-plane (y=0) point under a client-space position, or null when the
|
|
77
|
+
// pointer is off the canvas. Drives hotbar drag-placement and add-clicks.
|
|
78
|
+
export function raycastGroundPoint(ed, clientX, clientY) {
|
|
79
|
+
const rect = ed.canvas.getBoundingClientRect();
|
|
80
|
+
if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
|
84
|
+
const y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
85
|
+
const raycaster = new THREE.Raycaster();
|
|
86
|
+
raycaster.setFromCamera(new THREE.Vector2(x, y), ed.world.camera);
|
|
87
|
+
const point = new THREE.Vector3();
|
|
88
|
+
const hit = raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), point);
|
|
89
|
+
return hit ? { x: point.x, z: point.z } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wireGizmo(ed) {
|
|
93
|
+
ed.gizmo.addEventListener('dragging-changed', (event) => {
|
|
94
|
+
if (!event.value) ed.gizmoDraggedUntil = performance.now() + 150;
|
|
95
|
+
ed.orbit.enabled = !event.value;
|
|
96
|
+
if (event.value) beginGizmoDrag(ed);
|
|
97
|
+
else endGizmoDrag(ed);
|
|
98
|
+
});
|
|
99
|
+
ed.gizmo.addEventListener('objectChange', () => {
|
|
100
|
+
if (ed.gizmoDragging) applyGizmoChange(ed);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function beginGizmoDrag(ed) {
|
|
105
|
+
ed.gizmoDragging = true;
|
|
106
|
+
const primaryNode = ed.gizmo.object;
|
|
107
|
+
const startPositions = new Map();
|
|
108
|
+
for (const id of ed.selection) {
|
|
109
|
+
const t = editorActorTransform(ed, id);
|
|
110
|
+
if (t) startPositions.set(id, { x: t.x, y: t.y, z: t.z });
|
|
111
|
+
}
|
|
112
|
+
ed.dragStart = { primary: primaryNode.position.clone(), startPositions };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The gizmo moves the primary node directly; mirror that into the display
|
|
116
|
+
// runtime's Transforms (position deltas ripple to the rest of the selection in
|
|
117
|
+
// move mode) so the per-frame sync doesn't snap it back.
|
|
118
|
+
function applyGizmoChange(ed) {
|
|
119
|
+
const primaryId = ed.selection[0];
|
|
120
|
+
const node = ed.gizmo.object;
|
|
121
|
+
const t = editorActorTransform(ed, primaryId);
|
|
122
|
+
if (!node || !t || !ed.dragStart) return;
|
|
123
|
+
t.x = node.position.x;
|
|
124
|
+
t.y = node.position.y;
|
|
125
|
+
t.z = node.position.z;
|
|
126
|
+
t.rotationX = THREE.MathUtils.radToDeg(node.rotation.x);
|
|
127
|
+
t.rotationY = THREE.MathUtils.radToDeg(node.rotation.y);
|
|
128
|
+
t.rotationZ = THREE.MathUtils.radToDeg(node.rotation.z);
|
|
129
|
+
if (ed.gizmo.mode === 'translate') {
|
|
130
|
+
const delta = node.position.clone().sub(ed.dragStart.primary);
|
|
131
|
+
for (const id of ed.selection.slice(1)) {
|
|
132
|
+
const start = ed.dragStart.startPositions.get(id);
|
|
133
|
+
const other = editorActorTransform(ed, id);
|
|
134
|
+
if (!start || !other) continue;
|
|
135
|
+
other.x = start.x + delta.x;
|
|
136
|
+
other.y = start.y + delta.y;
|
|
137
|
+
other.z = start.z + delta.z;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function endGizmoDrag(ed) {
|
|
143
|
+
ed.gizmoDragging = false;
|
|
144
|
+
ed.dragStart = null;
|
|
145
|
+
const changes = [];
|
|
146
|
+
for (const id of ed.selection) {
|
|
147
|
+
const t = editorActorTransform(ed, id);
|
|
148
|
+
if (t) changes.push({ id, ...t });
|
|
149
|
+
}
|
|
150
|
+
if (changes.length) ed.callbacks.onCommitTransforms(changes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function wirePointer(ed) {
|
|
154
|
+
let downAt = null;
|
|
155
|
+
ed.canvas.addEventListener('pointerdown', (event) => {
|
|
156
|
+
downAt = { x: event.clientX, y: event.clientY };
|
|
157
|
+
});
|
|
158
|
+
ed.canvas.addEventListener('pointerup', (event) => {
|
|
159
|
+
if (!downAt) return;
|
|
160
|
+
const travel = Math.hypot(event.clientX - downAt.x, event.clientY - downAt.y);
|
|
161
|
+
downAt = null;
|
|
162
|
+
if (travel > CLICK_SLOP_PX || ed.gizmoDragging) return;
|
|
163
|
+
if (performance.now() < (ed.gizmoDraggedUntil ?? 0)) return;
|
|
164
|
+
handleClick(ed, event);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function rayFromEvent(ed, event) {
|
|
169
|
+
const rect = ed.canvas.getBoundingClientRect();
|
|
170
|
+
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
171
|
+
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
172
|
+
const raycaster = new THREE.Raycaster();
|
|
173
|
+
raycaster.setFromCamera(new THREE.Vector2(x, y), ed.world.camera);
|
|
174
|
+
return raycaster;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleClick(ed, event) {
|
|
178
|
+
const raycaster = rayFromEvent(ed, event);
|
|
179
|
+
if (ed.mode === 'add') {
|
|
180
|
+
const point = new THREE.Vector3();
|
|
181
|
+
raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), point);
|
|
182
|
+
if (point) ed.callbacks.onAddAt({ x: point.x, z: point.z });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const nodes = [...ed.world.nodes.values()].map((entry) => entry.node);
|
|
186
|
+
const intersects = raycaster.intersectObjects(nodes, true);
|
|
187
|
+
const hits = [];
|
|
188
|
+
for (const intersect of intersects) {
|
|
189
|
+
let object = intersect.object;
|
|
190
|
+
while (object && object.userData.actorId === undefined) object = object.parent;
|
|
191
|
+
const actorId = object?.userData.actorId;
|
|
192
|
+
if (actorId !== undefined && !hits.includes(actorId)) hits.push(actorId);
|
|
193
|
+
}
|
|
194
|
+
ed.callbacks.onPick(hits, event.shiftKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function syncSelectionBoxes(ed) {
|
|
198
|
+
for (const [id, entry] of ed.boxes) {
|
|
199
|
+
// Drop a box when its actor is deselected/gone OR its node was rebuilt
|
|
200
|
+
// (art or shape changed) -- a helper bound to a dead node freezes at the
|
|
201
|
+
// node's last position instead of following the actor.
|
|
202
|
+
const nodeEntry = ed.world.nodes.get(id);
|
|
203
|
+
if (ed.selection.includes(id) && nodeEntry && nodeEntry.node === entry.node) continue;
|
|
204
|
+
ed.world.scene.remove(entry.helper);
|
|
205
|
+
entry.helper.dispose();
|
|
206
|
+
ed.boxes.delete(id);
|
|
207
|
+
}
|
|
208
|
+
for (const id of ed.selection) {
|
|
209
|
+
const nodeEntry = ed.world.nodes.get(id);
|
|
210
|
+
if (!nodeEntry) continue;
|
|
211
|
+
let entry = ed.boxes.get(id);
|
|
212
|
+
if (!entry) {
|
|
213
|
+
entry = { helper: new THREE.BoxHelper(nodeEntry.node, 0xffffff), node: nodeEntry.node };
|
|
214
|
+
ed.world.scene.add(entry.helper);
|
|
215
|
+
ed.boxes.set(id, entry);
|
|
216
|
+
}
|
|
217
|
+
entry.helper.update();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Keep the gizmo attached to the primary selection whenever a transform mode
|
|
222
|
+
// is active, and detached otherwise.
|
|
223
|
+
function syncGizmo(ed) {
|
|
224
|
+
const primary = ed.selection[0];
|
|
225
|
+
const node = primary !== undefined ? ed.world.nodes.get(primary)?.node : null;
|
|
226
|
+
const wantGizmo = node && (ed.mode === 'move' || ed.mode === 'rotate');
|
|
227
|
+
if (wantGizmo) {
|
|
228
|
+
ed.gizmo.mode = ed.mode === 'move' ? 'translate' : 'rotate';
|
|
229
|
+
if (ed.gizmo.object !== node) ed.gizmo.attach(node);
|
|
230
|
+
} else if (ed.gizmo.object) {
|
|
231
|
+
ed.gizmo.detach();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function startEditorLoop(ed) {
|
|
236
|
+
const fit = () => resizeWorld(ed.world, ed.canvas.clientWidth, ed.canvas.clientHeight);
|
|
237
|
+
ed.resizeObserver = new ResizeObserver(fit);
|
|
238
|
+
ed.resizeObserver.observe(ed.canvas);
|
|
239
|
+
fit();
|
|
240
|
+
const tick = () => {
|
|
241
|
+
ed.orbit.update();
|
|
242
|
+
if (ed.runtime) syncActors(ed.world, ed.runtime, ed.sprites);
|
|
243
|
+
syncGizmo(ed);
|
|
244
|
+
syncSelectionBoxes(ed);
|
|
245
|
+
renderWorld(ed.world);
|
|
246
|
+
ed.raf = requestAnimationFrame(tick);
|
|
247
|
+
};
|
|
248
|
+
ed.raf = requestAnimationFrame(tick);
|
|
249
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Mesh + material construction from a Shape component. Materials are
|
|
2
|
+
// MeshPhysicalMaterial throughout (the vase look): PBR with a small set of
|
|
3
|
+
// knobs, albedo optionally baked from a `.pxart` (or uploaded image) with
|
|
4
|
+
// nearest filtering so pixels stay crisp.
|
|
5
|
+
//
|
|
6
|
+
// `Shape.art` is either one ref for the whole shape, or (boxes only) a per-face
|
|
7
|
+
// object: `{ all, sides, front, back, left, right, top, bottom }`. A ref is a
|
|
8
|
+
// file path with an optional `#<frame>` suffix to pick a frame of a multi-frame
|
|
9
|
+
// pxart -- so one file can carry different faces as frames.
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import { renderArtFrame, artReady, artFrameCount } from '@imports/castle.physics-2d/engine/art';
|
|
12
|
+
|
|
13
|
+
// Baked textures cached per art object per frame (sprites are re-parsed only
|
|
14
|
+
// when the file changes, so object identity is a good key).
|
|
15
|
+
const textureCache = new WeakMap();
|
|
16
|
+
|
|
17
|
+
function albedoTexture(art, frame) {
|
|
18
|
+
if (!art || !artReady(art)) return null;
|
|
19
|
+
const frameIndex = Math.max(0, Math.min(frame ?? 0, artFrameCount(art) - 1));
|
|
20
|
+
let byFrame = textureCache.get(art);
|
|
21
|
+
if (!byFrame) {
|
|
22
|
+
byFrame = new Map();
|
|
23
|
+
textureCache.set(art, byFrame);
|
|
24
|
+
}
|
|
25
|
+
const cached = byFrame.get(frameIndex);
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
const canvas = document.createElement('canvas');
|
|
28
|
+
if (!renderArtFrame(art, frameIndex, canvas)) return null;
|
|
29
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
30
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
31
|
+
texture.magFilter = THREE.NearestFilter;
|
|
32
|
+
texture.minFilter = THREE.NearestFilter;
|
|
33
|
+
texture.generateMipmaps = false;
|
|
34
|
+
byFrame.set(frameIndex, texture);
|
|
35
|
+
return texture;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 'drawings/door.pxart#1' -> { path, frame }
|
|
39
|
+
function parseArtRef(ref) {
|
|
40
|
+
const hash = ref.lastIndexOf('#');
|
|
41
|
+
if (hash === -1) return { path: ref, frame: 0 };
|
|
42
|
+
const frame = Number(ref.slice(hash + 1));
|
|
43
|
+
if (!Number.isInteger(frame) || frame < 0) return { path: ref, frame: 0 };
|
|
44
|
+
return { path: ref.slice(0, hash), frame };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveArtTexture(ref, sprites, repeat) {
|
|
48
|
+
if (!ref || typeof ref !== 'string') return null;
|
|
49
|
+
const { path, frame } = parseArtRef(ref);
|
|
50
|
+
const texture = albedoTexture(sprites?.[path], frame);
|
|
51
|
+
if (!texture || !repeat) return texture;
|
|
52
|
+
// Tiling wants its own texture instance -- the cached one is shared by
|
|
53
|
+
// non-repeating users. clone() shares the underlying image.
|
|
54
|
+
const tiled = texture.clone();
|
|
55
|
+
tiled.wrapS = THREE.RepeatWrapping;
|
|
56
|
+
tiled.wrapT = THREE.RepeatWrapping;
|
|
57
|
+
tiled.repeat.set(repeat[0] ?? 1, repeat[1] ?? 1);
|
|
58
|
+
tiled.needsUpdate = true;
|
|
59
|
+
return tiled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function artRefsOf(shape) {
|
|
63
|
+
if (!shape?.art) return [];
|
|
64
|
+
if (typeof shape.art === 'string') return [shape.art];
|
|
65
|
+
return Object.values(shape.art).filter((ref) => typeof ref === 'string');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stable id per parsed art OBJECT. A repainted pxart re-parses into a fresh
|
|
69
|
+
// object, so identity (not just readiness) has to be part of a signature --
|
|
70
|
+
// otherwise an edited texture never reaches an already-built mesh.
|
|
71
|
+
let artIdCounter = 0;
|
|
72
|
+
const artIds = new WeakMap();
|
|
73
|
+
export function artRefSignature(ref, sprites) {
|
|
74
|
+
const art = sprites?.[parseArtRef(typeof ref === 'string' ? ref : '').path];
|
|
75
|
+
if (!art) return '0';
|
|
76
|
+
let id = artIds.get(art);
|
|
77
|
+
if (!id) {
|
|
78
|
+
id = ++artIdCounter;
|
|
79
|
+
artIds.set(art, id);
|
|
80
|
+
}
|
|
81
|
+
return `${id}:${artReady(art)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// What about a Shape (plus the art files it references) requires a mesh
|
|
85
|
+
// rebuild. Cheap to compute every frame; rebuilds only happen when it changes.
|
|
86
|
+
export function shapeSignature(shape, sprites) {
|
|
87
|
+
if (!shape) return 'none';
|
|
88
|
+
const arts = artRefsOf(shape).map((ref) => artRefSignature(ref, sprites));
|
|
89
|
+
return JSON.stringify([shape, arts]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Everything that decides what an actor's node LOOKS like: the Shape, plus a
|
|
93
|
+
// Model (which replaces the shape mesh with a .pxmodel rig) -- including the
|
|
94
|
+
// model file's text and the readiness of any art it references. The model
|
|
95
|
+
// pieces are supplied by the caller (world3d) to keep this module below the
|
|
96
|
+
// model format.
|
|
97
|
+
export function nodeSignature(actor, sprites, modelSig = '') {
|
|
98
|
+
const model = actor.components.Model;
|
|
99
|
+
return `${shapeSignature(actor.components.Shape, sprites)}|${JSON.stringify(model ?? null)}|${modelSig}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shapeGeometry(shape) {
|
|
103
|
+
if (shape.kind === 'sphere') return new THREE.SphereGeometry(shape.radius, 24, 16);
|
|
104
|
+
if (shape.kind === 'capsule') {
|
|
105
|
+
const mid = Math.max(0, shape.height - 2 * shape.radius);
|
|
106
|
+
return new THREE.CapsuleGeometry(shape.radius, mid, 8, 16);
|
|
107
|
+
}
|
|
108
|
+
if (shape.kind === 'cylinder') {
|
|
109
|
+
return new THREE.CylinderGeometry(shape.radius, shape.radius, shape.height, 24);
|
|
110
|
+
}
|
|
111
|
+
return new THREE.BoxGeometry(shape.width, shape.height, shape.depth);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function makeMaterial(shape, map) {
|
|
115
|
+
// A textured face keeps its art's colors; `tint` multiplies over it when
|
|
116
|
+
// set. Untextured faces (and shapes) are just `color`. Every field falls
|
|
117
|
+
// back explicitly -- a raw blueprint template has no defaultProps merge, and
|
|
118
|
+
// `new THREE.Color(undefined)` is WHITE, which once turned every textured
|
|
119
|
+
// thumbnail into a white-emissive blob.
|
|
120
|
+
const color = map ? shape.tint || '#ffffff' : (shape.color ?? '#c7cfdd');
|
|
121
|
+
return new THREE.MeshPhysicalMaterial({
|
|
122
|
+
color: new THREE.Color(color),
|
|
123
|
+
map: map ?? null,
|
|
124
|
+
roughness: shape.roughness ?? 0.7,
|
|
125
|
+
metalness: shape.metalness ?? 0,
|
|
126
|
+
clearcoat: shape.clearcoat ?? 0,
|
|
127
|
+
emissive: new THREE.Color(shape.emissive ?? '#000000'),
|
|
128
|
+
emissiveIntensity: shape.emissiveIntensity ?? 1,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Three's box material order: +x right, -x left, +y top, -y bottom, +z front,
|
|
133
|
+
// -z back. Unnamed faces fall back to `sides` (left/right/back), then `all`,
|
|
134
|
+
// then plain color. `back` deliberately does NOT mirror `front` -- a face
|
|
135
|
+
// texture on a head must not appear on the back of the head; a door that
|
|
136
|
+
// looks the same from both sides just names `back` explicitly.
|
|
137
|
+
function boxFaceMaterials(shape, art, sprites) {
|
|
138
|
+
const all = art.all;
|
|
139
|
+
const sides = art.sides ?? all;
|
|
140
|
+
const refs = [
|
|
141
|
+
art.right ?? sides,
|
|
142
|
+
art.left ?? sides,
|
|
143
|
+
art.top ?? all,
|
|
144
|
+
art.bottom ?? art.top ?? all,
|
|
145
|
+
art.front ?? all,
|
|
146
|
+
art.back ?? sides,
|
|
147
|
+
];
|
|
148
|
+
return refs.map((ref) => makeMaterial(shape, resolveArtTexture(ref, sprites, shape.artRepeat)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function makeShapeMesh(shape, sprites) {
|
|
152
|
+
const art = shape.art;
|
|
153
|
+
let material;
|
|
154
|
+
if (art && typeof art === 'object' && shape.kind === 'box') {
|
|
155
|
+
material = boxFaceMaterials(shape, art, sprites);
|
|
156
|
+
} else {
|
|
157
|
+
material = makeMaterial(shape, resolveArtTexture(art, sprites, shape.artRepeat));
|
|
158
|
+
}
|
|
159
|
+
const mesh = new THREE.Mesh(shapeGeometry(shape), material);
|
|
160
|
+
mesh.castShadow = true;
|
|
161
|
+
mesh.receiveShadow = true;
|
|
162
|
+
return mesh;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function disposeNode(node) {
|
|
166
|
+
node.traverse((child) => {
|
|
167
|
+
if (child.isMesh) {
|
|
168
|
+
child.geometry?.dispose();
|
|
169
|
+
// Textures stay cached (shared across meshes); only the material goes.
|
|
170
|
+
if (Array.isArray(child.material)) child.material.forEach((m) => m.dispose());
|
|
171
|
+
else child.material?.dispose();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Pure operations on `kind: "mesh"` pxmodel parts -- an editable polygon mesh
|
|
2
|
+
// (vertices + quad-dominant faces, indices CCW-outward), the flexible core of
|
|
3
|
+
// the model format. Inspired by real modeling workflows (adjacency-aware
|
|
4
|
+
// editing, face extrusion); all code here is original. Faces may be tris,
|
|
5
|
+
// quads, or n-gons; rendering fan-triangulates with flat normals.
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
|
|
8
|
+
// A unit-ish editable mesh from a box part's size -- the "convert to mesh"
|
|
9
|
+
// seed. 8 vertices, 6 quads, outward CCW winding.
|
|
10
|
+
export function boxMesh(size) {
|
|
11
|
+
const [w, h, d] = size;
|
|
12
|
+
const x = w / 2;
|
|
13
|
+
const y = h / 2;
|
|
14
|
+
const z = d / 2;
|
|
15
|
+
const vertices = [
|
|
16
|
+
[-x, -y, -z],
|
|
17
|
+
[x, -y, -z],
|
|
18
|
+
[x, y, -z],
|
|
19
|
+
[-x, y, -z],
|
|
20
|
+
[-x, -y, z],
|
|
21
|
+
[x, -y, z],
|
|
22
|
+
[x, y, z],
|
|
23
|
+
[-x, y, z],
|
|
24
|
+
];
|
|
25
|
+
const faces = [
|
|
26
|
+
[3, 2, 1, 0], // -z back
|
|
27
|
+
[4, 5, 6, 7], // +z front
|
|
28
|
+
[1, 2, 6, 5], // +x
|
|
29
|
+
[0, 4, 7, 3], // -x
|
|
30
|
+
[3, 7, 6, 2], // +y
|
|
31
|
+
[0, 1, 5, 4], // -y
|
|
32
|
+
];
|
|
33
|
+
return { vertices, faces };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function faceCentroid(part, face) {
|
|
37
|
+
const c = [0, 0, 0];
|
|
38
|
+
for (const index of face) {
|
|
39
|
+
const v = part.vertices[index];
|
|
40
|
+
c[0] += v[0];
|
|
41
|
+
c[1] += v[1];
|
|
42
|
+
c[2] += v[2];
|
|
43
|
+
}
|
|
44
|
+
return c.map((value) => value / face.length);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function faceNormal(part, face) {
|
|
48
|
+
const a = part.vertices[face[0]];
|
|
49
|
+
const b = part.vertices[face[1]];
|
|
50
|
+
const c = part.vertices[face[2]];
|
|
51
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
52
|
+
const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
53
|
+
const n = [
|
|
54
|
+
ab[1] * ac[2] - ab[2] * ac[1],
|
|
55
|
+
ab[2] * ac[0] - ab[0] * ac[2],
|
|
56
|
+
ab[0] * ac[1] - ab[1] * ac[0],
|
|
57
|
+
];
|
|
58
|
+
const len = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
59
|
+
return [n[0] / len, n[1] / len, n[2] / len];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extrude one face outward along its normal: the face's vertices are
|
|
63
|
+
// duplicated and pushed out, the face re-points at the duplicates, and a side
|
|
64
|
+
// quad is stitched along each original edge. Adjacent selected faces extrude
|
|
65
|
+
// independently (region extrusion is a planned refinement).
|
|
66
|
+
export function extrudeFace(part, faceIndex, distance) {
|
|
67
|
+
const face = part.faces[faceIndex];
|
|
68
|
+
if (!face) return null;
|
|
69
|
+
const normal = faceNormal(part, face);
|
|
70
|
+
const start = part.vertices.length;
|
|
71
|
+
const lifted = face.map((index, i) => {
|
|
72
|
+
const v = part.vertices[index];
|
|
73
|
+
part.vertices.push([
|
|
74
|
+
round3(v[0] + normal[0] * distance),
|
|
75
|
+
round3(v[1] + normal[1] * distance),
|
|
76
|
+
round3(v[2] + normal[2] * distance),
|
|
77
|
+
]);
|
|
78
|
+
return start + i;
|
|
79
|
+
});
|
|
80
|
+
for (let i = 0; i < face.length; i++) {
|
|
81
|
+
const j = (i + 1) % face.length;
|
|
82
|
+
part.faces.push([face[i], face[j], lifted[j], lifted[i]]);
|
|
83
|
+
}
|
|
84
|
+
part.faces[faceIndex] = lifted;
|
|
85
|
+
return part.faces.length ? faceIndex : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const round3 = (v) => Math.round(v * 1000) / 1000;
|
|
89
|
+
|
|
90
|
+
export function roundMeshInPlace(part) {
|
|
91
|
+
part.vertices = part.vertices.map((v) => v.map(round3));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fan-triangulated flat-shaded geometry, plus a map from render triangle
|
|
95
|
+
// index back to the owning face (for raycast picking in the editor).
|
|
96
|
+
export function buildMeshGeometry(part) {
|
|
97
|
+
const positions = [];
|
|
98
|
+
const triFaces = [];
|
|
99
|
+
for (let faceIndex = 0; faceIndex < part.faces.length; faceIndex++) {
|
|
100
|
+
const face = part.faces[faceIndex];
|
|
101
|
+
if (!face || face.length < 3) continue;
|
|
102
|
+
for (let i = 1; i + 1 < face.length; i++) {
|
|
103
|
+
for (const index of [face[0], face[i], face[i + 1]]) {
|
|
104
|
+
const v = part.vertices[index] ?? [0, 0, 0];
|
|
105
|
+
positions.push(v[0], v[1], v[2]);
|
|
106
|
+
}
|
|
107
|
+
triFaces.push(faceIndex);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const geometry = new THREE.BufferGeometry();
|
|
111
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
112
|
+
geometry.computeVertexNormals();
|
|
113
|
+
return { geometry, triFaces };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// All vertex indices used by a set of faces.
|
|
117
|
+
export function faceVertexIndices(part, faceIndices) {
|
|
118
|
+
const out = new Set();
|
|
119
|
+
for (const faceIndex of faceIndices) {
|
|
120
|
+
for (const index of part.faces[faceIndex] ?? []) out.add(index);
|
|
121
|
+
}
|
|
122
|
+
return [...out];
|
|
123
|
+
}
|