lecodes-sdk 1.2.0 → 2.0.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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/src/scene/defineScene.ts
CHANGED
|
@@ -31,14 +31,18 @@
|
|
|
31
31
|
// sources are instantiated for real so the viewport shows the scene, but aspects are held as data
|
|
32
32
|
// (`node._sceneAspects`) WITHOUT attaching — no onAttach side effects (physics bodies, timers), no
|
|
33
33
|
// update() ticks. Defined handles register on `globalThis.__lecodesScenes` for the host to pick up.
|
|
34
|
+
//
|
|
35
|
+
// Edit mode exists in EDITOR BUNDLES only. Every edit-mode branch here reads `EDITOR && isEditMode()`
|
|
36
|
+
// — `EDITOR` is the compile-time half (bundler.ts: `false` in a shipped bundle, where chisel folds
|
|
37
|
+
// the branch away and tree-shakes what only it reached), the flag the run-time one. What the editor
|
|
38
|
+
// DOES to a loaded scene (patch / rename / live props / inspector cards) is not here at all: it is
|
|
39
|
+
// ./editor.ts, which only an editor bundle injects. The `@internal` exports below are its seam.
|
|
34
40
|
|
|
35
|
-
import { Aspect, type
|
|
36
|
-
import { describeAspect, describeFields, type AspectClassInfo } from "../core/fields"
|
|
41
|
+
import { Aspect, type With } from "../core/Aspect"
|
|
37
42
|
import {
|
|
38
43
|
collectRefDeps, isEditMode, isNodeRef, resolveRefPath, resolveRefs, use, ref, make, EDIT_FLAG,
|
|
39
44
|
type AspectEntry, type MakeEntry as SharedMakeEntry,
|
|
40
45
|
} from "./grammar"
|
|
41
|
-
import { InspectorUI, type InspectorEvent, type InspectorWidget } from "../core/InspectorUI"
|
|
42
46
|
import type { Vec3Like } from "../math/vec"
|
|
43
47
|
import { Scene, type SceneOptions } from "../gl/Scene"
|
|
44
48
|
import { CAMERA_DEFAULTS } from "../gl/Camera"
|
|
@@ -56,7 +60,7 @@ import { Light, type SunOptions } from "../gl/Light"
|
|
|
56
60
|
import { Material } from "../gl/Material"
|
|
57
61
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
58
62
|
import type { CapsuleOptions, CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
59
|
-
import { GizmoBuffer, Gizmos, withGizmoScope
|
|
63
|
+
import { GizmoBuffer, Gizmos, withGizmoScope } from "./gizmos"
|
|
60
64
|
|
|
61
65
|
// ---- the literal grammar (what the visual editor reads and writes) -----------
|
|
62
66
|
|
|
@@ -207,7 +211,7 @@ export type SceneCameraDef = CameraProjectionDef & {
|
|
|
207
211
|
target?: Vec3Like
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
/** `env.lightmap` — the level's baked lighting (
|
|
214
|
+
/** `env.lightmap` — the level's baked lighting (engines/bake): the files `lecodes lightmap bake`
|
|
211
215
|
* writes. Absent = real-time only. */
|
|
212
216
|
export type SceneLightmapDef = {
|
|
213
217
|
/** `asset('./assets/lightmap/level.bake')` */
|
|
@@ -312,7 +316,7 @@ export type LoadedScene<D extends SceneDef> = {
|
|
|
312
316
|
// A model's internal hierarchy is addressed by PART PATHS — '/'-joined segments from the model
|
|
313
317
|
// root down, where a segment is the child's name, disambiguated as `name[i]` among same-named
|
|
314
318
|
// siblings and `[i]` for unnamed children (i = index within that same-named group). The editor
|
|
315
|
-
// enumerates rows via `
|
|
319
|
+
// enumerates rows via `sceneEditor.modelParts` (./editor.ts) and writes the paths as `overrides` keys; the
|
|
316
320
|
// loader resolves them back through the same enumeration, so writer and resolver can't drift.
|
|
317
321
|
|
|
318
322
|
/** One instance of a scene file built as a subtree (`handle.instantiate`). */
|
|
@@ -402,7 +406,7 @@ const resolveMountNode = (parent: Node, mountPath: string, ownerPath: string): N
|
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
/** Where a def node attaches: its `mount` part when it resolves, else the parent itself. */
|
|
405
|
-
const attachHost = (parent: Node, def: SceneNodeDef, path: string): Node =>
|
|
409
|
+
export const attachHost = (parent: Node, def: SceneNodeDef, path: string): Node =>
|
|
406
410
|
def.mount !== undefined ? resolveMountNode(parent, def.mount, path) ?? parent : parent
|
|
407
411
|
|
|
408
412
|
// ---- runtime -------------------------------------------------------------------
|
|
@@ -478,7 +482,8 @@ const loadModel = (source: string, lightmap: boolean, foliage: boolean): Promise
|
|
|
478
482
|
return template.then((t) => t.clone())
|
|
479
483
|
}
|
|
480
484
|
|
|
481
|
-
|
|
485
|
+
/** @internal */
|
|
486
|
+
export const createSource = (path: string, def: SceneNodeDef, lightmap: boolean): Node | Promise<Node> => {
|
|
482
487
|
const sources = [ def.mesh, def.model, def.light, def.make, def.prefab, def.camera, def.terrain ].filter((s) => s !== undefined).length
|
|
483
488
|
if (sources > 1) throw new Error(`Scene node "${path}" declares more than one source (mesh/model/light/make/prefab/camera/terrain)`)
|
|
484
489
|
if (def.model !== undefined) return loadModel(def.model, lightmap, !!def.foliage)
|
|
@@ -510,7 +515,8 @@ export const applyEditorPose = (node: Node, pose: { clip?: string, time?: number
|
|
|
510
515
|
}
|
|
511
516
|
}
|
|
512
517
|
|
|
513
|
-
|
|
518
|
+
/** @internal */
|
|
519
|
+
export const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
514
520
|
node.name = name
|
|
515
521
|
// def-built marker: part enumeration must skip this node (it is not asset-internal — it has
|
|
516
522
|
// its own def path), even when it is parented inside a model via `mount`
|
|
@@ -520,8 +526,8 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
520
526
|
if (def.scale !== undefined) node.scale = def.scale
|
|
521
527
|
if (def.visible !== undefined) node.visible = def.visible
|
|
522
528
|
// editor-only flag (the harness's manipulation layer consults it); inert at runtime
|
|
523
|
-
if (def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
|
|
524
|
-
if (def.editor !== undefined && isEditMode()) applyEditorPose(node, def.editor)
|
|
529
|
+
if (EDITOR && def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
|
|
530
|
+
if (EDITOR && def.editor !== undefined && isEditMode()) applyEditorPose(node, def.editor)
|
|
525
531
|
if (def.camera !== undefined) (node as { _sceneCamera?: boolean })._sceneCamera = true
|
|
526
532
|
// waypoint/def order for aspects that read children (FollowPath) — the engine's live child
|
|
527
533
|
// order is insertion-based and may not match the file
|
|
@@ -552,9 +558,9 @@ const isGroupDef = (def: SceneNodeDef): boolean =>
|
|
|
552
558
|
* an axis cross / a frustum in the node's local frame (scene/gizmos.ts) — so they show and pick
|
|
553
559
|
* in the viewport. Never scene content: nothing renders in Filament, nothing outlines, and the
|
|
554
560
|
* engine follows the node live, so the buffer is filled once and lives on the node
|
|
555
|
-
* (`_editorMarker`) — `
|
|
561
|
+
* (`_editorMarker`) — `sceneEditor.gizmos()` reads it off the handle's own def nodes, so a removed
|
|
556
562
|
* node's marker goes with its record and prefab / instance internals (not selectable) draw none. */
|
|
557
|
-
const addEditorMarker = (node: Node, def: SceneNodeDef): void => {
|
|
563
|
+
export const addEditorMarker = (node: Node, def: SceneNodeDef): void => {
|
|
558
564
|
const buffer = new GizmoBuffer()
|
|
559
565
|
if (def.camera !== undefined) {
|
|
560
566
|
withGizmoScope(buffer, () => Gizmos.frustum(def.camera?.fov ?? CAMERA_DEFAULTS.fov, { node, color: "#cfd4dd" }))
|
|
@@ -603,7 +609,7 @@ const findCamera = (defs?: Record<string, SceneNodeDef>, prefix = ""): { path: s
|
|
|
603
609
|
/** fov / near / far from a camera block onto the live camera. The build path leaves an empty block
|
|
604
610
|
* alone (a host may run with its own configured fov); `reset` — the editor's live patch — fills
|
|
605
611
|
* omitted keys with the defaults instead, so clearing a field in the inspector takes effect. */
|
|
606
|
-
const applyCameraProjection = (scene: Scene, def: CameraProjectionDef, reset = false): void => {
|
|
612
|
+
export const applyCameraProjection = (scene: Scene, def: CameraProjectionDef, reset = false): void => {
|
|
607
613
|
const { fov, near, far } = def
|
|
608
614
|
if (!reset && fov === undefined && near === undefined && far === undefined) return
|
|
609
615
|
scene.camera.setProjection(reset
|
|
@@ -614,8 +620,8 @@ const applyCameraProjection = (scene: Scene, def: CameraProjectionDef, reset = f
|
|
|
614
620
|
// ---- editor-run aspects (generators) ------------------------------------------------------------
|
|
615
621
|
// A class with `static editor = { rebuild: true }` runs while a scene is edited: the loader
|
|
616
622
|
// constructs it (refs resolved, node + `generated` set — NEVER onAttach) and calls rebuild(); the
|
|
617
|
-
// editor re-runs rebuild() on inspector prop edits (`
|
|
618
|
-
// ref() fields point at changes (`
|
|
623
|
+
// editor re-runs rebuild() on inspector prop edits (`sceneEditor.setProp`, ./editor.ts) and whenever a
|
|
624
|
+
// node its ref() fields point at changes (`sceneEditor.nodeChanged` — dep tracking is derived from the props).
|
|
619
625
|
// Generated output lives under `this.generated`, a scene-added container child: never written to
|
|
620
626
|
// the file, absent from the doc tree — the file stores the recipe, the viewport shows the result.
|
|
621
627
|
|
|
@@ -637,7 +643,7 @@ class GeneratedGroup extends Node {
|
|
|
637
643
|
clear(): this {
|
|
638
644
|
this.version++
|
|
639
645
|
for (const c of [ ...this.children ]) {
|
|
640
|
-
dropForeignRuns(c)
|
|
646
|
+
if (EDITOR) dropForeignRuns(c)
|
|
641
647
|
this._scene.remove(c)
|
|
642
648
|
c.destroy()
|
|
643
649
|
}
|
|
@@ -649,7 +655,7 @@ class GeneratedGroup extends Node {
|
|
|
649
655
|
// arms' gun socket) are not the edited handle's runs — not inspector-addressable, not
|
|
650
656
|
// dep-tracked — but their gizmos (the weapon's sight line) must still draw. They register here
|
|
651
657
|
// keyed by the instance root; `dispose()` / a hosting `generated.clear()` drops them.
|
|
652
|
-
const foreignRuns = new Map<Node, EditorRun[]>()
|
|
658
|
+
export const foreignRuns = new Map<Node, EditorRun[]>()
|
|
653
659
|
const dropForeignRuns = (root: Node): void => {
|
|
654
660
|
for (const key of [ ...foreignRuns.keys() ]) {
|
|
655
661
|
let n: Node | null = key
|
|
@@ -668,7 +674,7 @@ const makeGenerated = (node: Node, scene: Scene): GeneratedGroup => {
|
|
|
668
674
|
}
|
|
669
675
|
|
|
670
676
|
/** One live editor-run aspect instance (edit mode only). */
|
|
671
|
-
type EditorRun = {
|
|
677
|
+
export type EditorRun = {
|
|
672
678
|
/** Absolute path of the host def node (re-keyed on rename/reparent). */
|
|
673
679
|
hostPath: string
|
|
674
680
|
node: Node
|
|
@@ -677,7 +683,7 @@ type EditorRun = {
|
|
|
677
683
|
inst: { rebuild?(): void | Promise<void> }
|
|
678
684
|
/** Async rebuild() supersession counter (see safeRebuild). */
|
|
679
685
|
generation: number
|
|
680
|
-
/** Mutable props snapshot — `
|
|
686
|
+
/** Mutable props snapshot — `sceneEditor.setProp` updates it and re-derives `deps`. Holds the
|
|
681
687
|
* DOC-LITERAL `$ref` strings (never resolved paths): the inspector's doc-sync compares these
|
|
682
688
|
* against the file's props, so rewriting them would re-fire on every render. */
|
|
683
689
|
props: Record<string, unknown>
|
|
@@ -690,8 +696,13 @@ type EditorRun = {
|
|
|
690
696
|
|
|
691
697
|
/** Bumped on every editor-run rebuild — the host compares it to know when to re-push gizmos. */
|
|
692
698
|
let gizmoVersion = 0
|
|
699
|
+
/** @internal */
|
|
700
|
+
export const bumpGizmoVersion = (): number => ++gizmoVersion
|
|
701
|
+
/** @internal */
|
|
702
|
+
export const currentGizmoVersion = (): number => gizmoVersion
|
|
693
703
|
|
|
694
|
-
|
|
704
|
+
/** @internal */
|
|
705
|
+
export const safeRebuild = (run: EditorRun): Promise<void> | void => {
|
|
695
706
|
// a throwing generator must not take the editor session down with it; the gizmo scope is the
|
|
696
707
|
// SYNCHRONOUS part of the call — whatever rebuild draws replaces the run's previous lines. An
|
|
697
708
|
// async rebuild() (one that instantiates a scene file) is awaited; lines it wants to draw after
|
|
@@ -714,7 +725,7 @@ const safeRebuild = (run: EditorRun): Promise<void> | void => {
|
|
|
714
725
|
|
|
715
726
|
/** Re-assign every ref-carrying prop from the CURRENT nodes map — a live patch replaces node
|
|
716
727
|
* instances, so a generator's resolved fields would otherwise point at destroyed nodes. */
|
|
717
|
-
const assignRefProps = (run: EditorRun, nodes: Record<string, Node>): void => {
|
|
728
|
+
export const assignRefProps = (run: EditorRun, nodes: Record<string, Node>): void => {
|
|
718
729
|
const lookup = (r: string): Node | null => {
|
|
719
730
|
const p = resolveRefPath(nodes, run.hostPath, r)
|
|
720
731
|
return p === null ? null : nodes[p]
|
|
@@ -735,7 +746,7 @@ const assignRefProps = (run: EditorRun, nodes: Record<string, Node>): void => {
|
|
|
735
746
|
// machinery generator aspects use — live, no compile (the code is already in the bundle).
|
|
736
747
|
|
|
737
748
|
/** EditorRun.index for a node's make() run (aspect runs use their array index, always >= 0). */
|
|
738
|
-
const MAKE_INDEX = -1
|
|
749
|
+
export const MAKE_INDEX = -1
|
|
739
750
|
|
|
740
751
|
const createMakeInst = (path: string, entry: MakeEntry<any>, generated: GeneratedGroup): { rebuild(): void } => {
|
|
741
752
|
let token = 0
|
|
@@ -743,7 +754,7 @@ const createMakeInst = (path: string, entry: MakeEntry<any>, generated: Generate
|
|
|
743
754
|
inst.rebuild = () => {
|
|
744
755
|
const t = ++token
|
|
745
756
|
generated.clear()
|
|
746
|
-
// current args = the instance's own fields (that's where
|
|
757
|
+
// current args = the instance's own fields (that's where sceneEditor.setProp / assignRefProps write)
|
|
747
758
|
const args: Record<string, unknown> = {}
|
|
748
759
|
for (const [ k, v ] of Object.entries(inst)) if (k !== "rebuild") args[k] = v
|
|
749
760
|
const result = entry.fn(args as never)
|
|
@@ -766,7 +777,7 @@ const attachMake = (
|
|
|
766
777
|
const entry = def.make
|
|
767
778
|
if (!entry) return undefined
|
|
768
779
|
const generated = makeGenerated(node, scene)
|
|
769
|
-
if (isEditMode()) {
|
|
780
|
+
if (EDITOR && isEditMode()) {
|
|
770
781
|
const props = { ...(entry.args ?? {}) } as Record<string, unknown>
|
|
771
782
|
const inst = createMakeInst(path, entry, generated)
|
|
772
783
|
Object.assign(inst, resolveRefs(props, nodes, path))
|
|
@@ -781,11 +792,12 @@ const attachMake = (
|
|
|
781
792
|
})
|
|
782
793
|
}
|
|
783
794
|
|
|
784
|
-
|
|
795
|
+
/** @internal */
|
|
796
|
+
export const attachAspects = (
|
|
785
797
|
node: Node, path: string, def: SceneNodeDef,
|
|
786
798
|
nodes: Record<string, Node>, scene: Scene, editorRuns: EditorRun[],
|
|
787
799
|
): Promise<void> | void => {
|
|
788
|
-
if (isEditMode()) {
|
|
800
|
+
if (EDITOR && isEditMode()) {
|
|
789
801
|
const waits: Promise<void>[] = []
|
|
790
802
|
// Data-only: the inspector reads [ctor, props] from here; nothing attaches, so no onAttach side
|
|
791
803
|
// effects (physics bodies, loops) run while editing. `ref()` markers stay unresolved data too.
|
|
@@ -840,23 +852,26 @@ const prefabPartRows = (defs: Record<string, SceneNodeDef>, local: Record<string
|
|
|
840
852
|
const node = local[path]
|
|
841
853
|
if (!node) continue
|
|
842
854
|
out.push({ path, name, depth, node })
|
|
843
|
-
|
|
855
|
+
// no argument spreads here: a 78 000-node level (an imported Unity scene, every rock and grass tuft
|
|
856
|
+
// a placement) blew QuickJS's 65 534-argument cap on the top-level prefab's rows
|
|
857
|
+
for (const r of prefabPartRows(nd.children ?? {}, local, path, depth + 1)) out.push(r)
|
|
844
858
|
const inner = node instanceof Model
|
|
845
859
|
? modelPartRows(node)
|
|
846
860
|
: (node as { _prefabParts?: ModelPartRow[] })._prefabParts
|
|
847
|
-
if (inner)
|
|
861
|
+
if (inner) for (const r of inner) out.push({ ...r, path: `${path}/${r.path}`, depth: depth + 1 + r.depth })
|
|
848
862
|
}
|
|
849
863
|
return out
|
|
850
864
|
}
|
|
851
865
|
|
|
852
|
-
|
|
866
|
+
/** @internal */
|
|
867
|
+
export const attachPrefab = async (wrapper: Node, path: string, def: SceneNodeDef, scene: Scene, stack: Set<SceneDef>, lm: LightmapCtx | null): Promise<void> => {
|
|
853
868
|
const pdef = (def.prefab as { def?: SceneDef } | undefined)?.def
|
|
854
869
|
if (!pdef || typeof pdef !== "object") {
|
|
855
870
|
console.error(`[scene] "${path}".prefab is not a scene handle (import the .scene file's default export)`)
|
|
856
871
|
return
|
|
857
872
|
}
|
|
858
|
-
// editor marker: the harness enumerates prefab internals as parts, like a GLB's (`
|
|
859
|
-
|
|
873
|
+
// editor marker: the harness enumerates prefab internals as parts, like a GLB's (`sceneEditor.modelParts`)
|
|
874
|
+
if (EDITOR) (wrapper as { _scenePrefab?: boolean })._scenePrefab = true
|
|
860
875
|
if (stack.has(pdef)) {
|
|
861
876
|
console.error(`[scene] prefab cycle detected at "${path}" — instance skipped`)
|
|
862
877
|
return
|
|
@@ -903,7 +918,7 @@ const buildNodes = async (
|
|
|
903
918
|
else if (lmStatic && nd.terrain !== undefined) Lightmap._registerGround(node, lm!.prefix + path)
|
|
904
919
|
// the top of a moving subtree: everything under it reads the level's light grid (Lightmap.track walks it at load)
|
|
905
920
|
if (lm !== null && !lmStatic && !parentMoves) moverRoots.push(node)
|
|
906
|
-
if (isEditMode()) addEditorMarker(node, nd)
|
|
921
|
+
if (EDITOR && isEditMode()) addEditorMarker(node, nd)
|
|
907
922
|
// a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
|
|
908
923
|
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/` } : null)
|
|
909
924
|
pending.push({ path, node, def: nd })
|
|
@@ -971,10 +986,10 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
971
986
|
if (foliage) Foliage.configure(foliage)
|
|
972
987
|
const nodes: Record<string, Node> = {}
|
|
973
988
|
// baked lighting is a play-mode concern; the level owns the registry (a rebuild starts clean)
|
|
974
|
-
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: "" } : null
|
|
989
|
+
const lm: LightmapCtx | null = lightmap && !(EDITOR && isEditMode()) ? { prefix: "" } : null
|
|
975
990
|
if (lm) Lightmap.clear()
|
|
976
991
|
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]), lm)
|
|
977
|
-
if (navmesh && !isEditMode()) registerNavOverrides(def.nodes, nodes)
|
|
992
|
+
if (navmesh && !(EDITOR && isEditMode())) registerNavOverrides(def.nodes, nodes)
|
|
978
993
|
|
|
979
994
|
// a camera NODE wins over the top-level `camera:` block; in play mode it keeps driving the view
|
|
980
995
|
// (CameraRig), in edit mode it only seeds the editor's starting viewpoint. The PROJECTION applies
|
|
@@ -990,13 +1005,13 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
990
1005
|
scene.camera.position = placeNode.worldPosition
|
|
991
1006
|
scene.camera.quaternion = placeNode.worldQuaternion
|
|
992
1007
|
applyCameraProjection(scene, proj)
|
|
993
|
-
if (!isEditMode()) scene.camera.follow(placeNode)
|
|
1008
|
+
if (!(EDITOR && isEditMode())) scene.camera.follow(placeNode)
|
|
994
1009
|
} else if (cam && camNode) {
|
|
995
1010
|
// legacy: the `camera: {}` node source block (still honoured — prefer a CameraPlace aspect)
|
|
996
1011
|
scene.camera.position = camNode.worldPosition
|
|
997
1012
|
scene.camera.quaternion = camNode.worldQuaternion
|
|
998
1013
|
applyCameraProjection(scene, cam.def)
|
|
999
|
-
if (!isEditMode()) scene.camera.follow(camNode)
|
|
1014
|
+
if (!(EDITOR && isEditMode())) scene.camera.follow(camNode)
|
|
1000
1015
|
} else if (def.camera) {
|
|
1001
1016
|
if (def.camera.position) scene.camera.position = def.camera.position
|
|
1002
1017
|
if (def.camera.target) scene.camera.lookAt(def.camera.target)
|
|
@@ -1010,7 +1025,7 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
1010
1025
|
}
|
|
1011
1026
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
1012
1027
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
1013
|
-
if (navmesh && !isEditMode()) {
|
|
1028
|
+
if (navmesh && !(EDITOR && isEditMode())) {
|
|
1014
1029
|
await NavMesh.load(scene, { data: navmesh.data, agent: navmesh.agent, areas: navmesh.areas })
|
|
1015
1030
|
}
|
|
1016
1031
|
|
|
@@ -1020,14 +1035,13 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
1020
1035
|
export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
1021
1036
|
readonly def: D
|
|
1022
1037
|
private _loading?: Promise<LoadedScene<D>>
|
|
1023
|
-
/** @internal Live editor-run aspect instances (edit mode only — see attachAspects).
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
/** @internal The loaded scene + path-keyed node record, for the editor methods below (set once
|
|
1038
|
+
/** @internal Live editor-run aspect instances (edit mode only — see attachAspects). The editor
|
|
1039
|
+
* (./editor.ts) reads and replaces the list. */
|
|
1040
|
+
_editorRuns: EditorRun[] = []
|
|
1041
|
+
/** @internal The loaded scene + path-keyed node record, for the editor (./editor.ts; set once
|
|
1028
1042
|
* load resolves). The record object is SHARED with the returned LoadedScene — patches and
|
|
1029
1043
|
* rename/reparent re-keying are visible through both. */
|
|
1030
|
-
|
|
1044
|
+
_live: { scene: Scene, nodes: Record<string, Node> } | null = null
|
|
1031
1045
|
|
|
1032
1046
|
constructor(def: D) { this.def = def }
|
|
1033
1047
|
|
|
@@ -1070,10 +1084,10 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
1070
1084
|
// their generators' gizmos still draw (foreignRuns — a nested rig's sight line)
|
|
1071
1085
|
const runs: EditorRun[] = []
|
|
1072
1086
|
await buildNodes(this.def.nodes ?? {}, root, scene, nodes, runs, new Set([ this.def ]))
|
|
1073
|
-
if (isEditMode() && runs.length > 0) { foreignRuns.set(root, runs); gizmoVersion++ }
|
|
1087
|
+
if (EDITOR && isEditMode() && runs.length > 0) { foreignRuns.set(root, runs); gizmoVersion++ }
|
|
1074
1088
|
const get = (path: string): Node | null => nodes[path] ?? null
|
|
1075
1089
|
const dispose = (): void => {
|
|
1076
|
-
foreignRuns.delete(root)
|
|
1090
|
+
if (EDITOR) foreignRuns.delete(root)
|
|
1077
1091
|
root.visible = false // the visibility cascade retires the subtree's pick colliders first
|
|
1078
1092
|
scene.remove(root)
|
|
1079
1093
|
root.destroy()
|
|
@@ -1086,324 +1100,6 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
1086
1100
|
}
|
|
1087
1101
|
return { root, nodes, get, dispose, alignTo } as unknown as SceneInstance<D>
|
|
1088
1102
|
}
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* @internal Editor (edit mode): apply ONE node's change to the already-loaded scene without
|
|
1092
|
-
* recompiling — the same "scenes as data" grammar, but as a patch: `def` rebuilds the node at
|
|
1093
|
-
* `path` in place (or adds it when the path is new — the path's parent must exist, root paths
|
|
1094
|
-
* mount at the root), `def === null` removes it with its whole subtree. The def must be PLAIN
|
|
1095
|
-
* data — the editor falls back to a full re-run for `$expr` and `$asset` values; aspect changes
|
|
1096
|
-
* are inert in edit mode and stay out of defs.
|
|
1097
|
-
*
|
|
1098
|
-
* Named children survive a rebuild: they are re-parented onto the replacement node keeping their
|
|
1099
|
-
* local transforms (exactly what the scene file describes). Returns the fresh node, null for a
|
|
1100
|
-
* removal (or an add under an unknown parent). Old GPU resources (geometry/material instances)
|
|
1101
|
-
* are not reclaimed until the next full re-run — acceptable churn for an edit session.
|
|
1102
|
-
*/
|
|
1103
|
-
async _patchNode(path: string, def: SceneNodeDef | null): Promise<Node | null> {
|
|
1104
|
-
const { scene, nodes } = (await this.load()) as unknown as { scene: Scene, nodes: Record<string, Node> }
|
|
1105
|
-
const old: Node | undefined = nodes[path]
|
|
1106
|
-
|
|
1107
|
-
const dispose = (root: Node): void => {
|
|
1108
|
-
// Hide first: the visibility cascade deactivates the subtree's pick colliders (the ABI has
|
|
1109
|
-
// no removeCollider — a destroyed entity's stale collider entry must never pick again).
|
|
1110
|
-
root.visible = false
|
|
1111
|
-
const doomed = new Set<Node>()
|
|
1112
|
-
root.traverse((n) => doomed.add(n))
|
|
1113
|
-
for (const [ key, n ] of Object.entries(nodes)) if (doomed.has(n)) delete nodes[key]
|
|
1114
|
-
// editor-run aspect instances hosted in the doomed subtree go with it (generated children
|
|
1115
|
-
// are subtree children, so the destroy below reclaims them too)
|
|
1116
|
-
this._editorRuns = this._editorRuns.filter((r) => !doomed.has(r.node))
|
|
1117
|
-
gizmoVersion++
|
|
1118
|
-
scene.remove(root)
|
|
1119
|
-
root.destroy() // native destroyEntity recurses over remaining children
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (def === null) {
|
|
1123
|
-
if (old) {
|
|
1124
|
-
dispose(old)
|
|
1125
|
-
this._refreshRunDeps()
|
|
1126
|
-
}
|
|
1127
|
-
return null
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
const build = async (p: string, d: SceneNodeDef, parent: Node | null): Promise<Node> => {
|
|
1131
|
-
// child reuse is by FULL path — only the live subtree at this exact address is carried
|
|
1132
|
-
// over (a bare-name lookup would adopt a like-named node from anywhere in the scene)
|
|
1133
|
-
const existing = p === path ? undefined : nodes[p]
|
|
1134
|
-
if (existing) { // an already-live child subtree — keep it, just re-parent (local transform stays)
|
|
1135
|
-
if (parent) attachHost(parent, d, p).add(existing)
|
|
1136
|
-
return existing
|
|
1137
|
-
}
|
|
1138
|
-
const node = await createSource(p, d, false)
|
|
1139
|
-
if (parent) attachHost(parent, d, p).add(node)
|
|
1140
|
-
scene.add(node)
|
|
1141
|
-
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
|
1142
|
-
if (isEditMode()) addEditorMarker(node, d)
|
|
1143
|
-
// a prefab wrapper instantiates its file's nodes (same path as buildNodes; no lightmap
|
|
1144
|
-
// context — a patched node is an edit-mode rebuild, baked lighting is the level's business)
|
|
1145
|
-
if (d.prefab !== undefined) await attachPrefab(node, p, d, scene, new Set([ this.def ]), null)
|
|
1146
|
-
attachAspects(node, p, d, nodes, scene, this._editorRuns)
|
|
1147
|
-
nodes[p] = node
|
|
1148
|
-
await Promise.all(Object.entries(d.children ?? {}).map(([ cn, cd ]) => build(`${p}/${cn}`, cd, node)))
|
|
1149
|
-
return node
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const cut = path.lastIndexOf("/")
|
|
1153
|
-
const parentPath = cut < 0 ? null : path.slice(0, cut)
|
|
1154
|
-
if (!old && parentPath !== null && !nodes[parentPath]) return null
|
|
1155
|
-
const parent = old ? old.parent : (parentPath !== null ? nodes[parentPath] : null)
|
|
1156
|
-
const fresh = await build(path, def, parent)
|
|
1157
|
-
if (old) {
|
|
1158
|
-
// named children not listed in the def still move over (the editor patches one node at a time)
|
|
1159
|
-
const named = new Set(Object.values(nodes))
|
|
1160
|
-
for (const child of old.children) if (named.has(child)) fresh.add(child)
|
|
1161
|
-
dispose(old) // fresh already replaced nodes[path] in build(), so it survives
|
|
1162
|
-
nodes[path] = fresh
|
|
1163
|
-
}
|
|
1164
|
-
// the projection lives on the scene camera, not on the node — re-apply it here so an inspector
|
|
1165
|
-
// fov/near/far edit lands live (a rebuilt node alone would carry none of it)
|
|
1166
|
-
if (def.camera !== undefined) applyCameraProjection(scene, def.camera, true)
|
|
1167
|
-
this._refreshRunDeps()
|
|
1168
|
-
return fresh
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/** @internal Editor: every run's gizmo lines (world-space LINES batches) + a version that
|
|
1172
|
-
* changes whenever any rebuild ran — the host re-pushes to the engine only on a change. */
|
|
1173
|
-
_editorGizmos(): { version: number, batches: GizmoBatch[] } {
|
|
1174
|
-
const batches: GizmoBatch[] = []
|
|
1175
|
-
for (const run of this._editorRuns) for (const b of run.gizmos.batches) if (b.segments.length > 0) batches.push(b)
|
|
1176
|
-
for (const runs of foreignRuns.values()) for (const run of runs) for (const b of run.gizmos.batches) if (b.segments.length > 0) batches.push(b)
|
|
1177
|
-
for (const node of Object.values(this._live?.nodes ?? {})) {
|
|
1178
|
-
const marker = (node as { _editorMarker?: GizmoBuffer })._editorMarker
|
|
1179
|
-
if (marker) for (const b of marker.batches) batches.push(b)
|
|
1180
|
-
}
|
|
1181
|
-
return { version: gizmoVersion, batches }
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
/** @internal Re-derive every editor run's deps from the CURRENT record. Deps are RESOLVED
|
|
1185
|
-
* absolute paths, so any structural change can invalidate them: an add can satisfy a
|
|
1186
|
-
* previously-null ref, a remove/rename can re-bind one to a different scope (shadowing). */
|
|
1187
|
-
private _refreshRunDeps(): void {
|
|
1188
|
-
const nodes = this._live?.nodes
|
|
1189
|
-
if (!nodes) return
|
|
1190
|
-
for (const run of this._editorRuns) {
|
|
1191
|
-
run.deps = collectRefDeps(run.props, nodes, run.hostPath)
|
|
1192
|
-
// aspect runs keep their host as a dep (see attachAspects); make() runs must NOT — the
|
|
1193
|
-
// wrapper's transform is editor-owned and moving it never re-calls the factory
|
|
1194
|
-
if (run.index !== MAKE_INDEX) run.deps.add(run.hostPath)
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
/** @internal Re-key everything addressed under `oldPath` (the node itself, descendants, editor
|
|
1199
|
-
* runs, inspector cards) to `newPath`, then re-derive deps. The record object is shared with
|
|
1200
|
-
* the host — mutation, not replacement. */
|
|
1201
|
-
private _rekey(oldPath: string, newPath: string): void {
|
|
1202
|
-
const nodes = this._live!.nodes
|
|
1203
|
-
const move = (key: string): string | null =>
|
|
1204
|
-
key === oldPath ? newPath
|
|
1205
|
-
: key.startsWith(oldPath + "/") ? newPath + key.slice(oldPath.length)
|
|
1206
|
-
: null
|
|
1207
|
-
for (const key of Object.keys(nodes)) {
|
|
1208
|
-
const next = move(key)
|
|
1209
|
-
if (next === null) continue
|
|
1210
|
-
const n = nodes[key]
|
|
1211
|
-
delete nodes[key]
|
|
1212
|
-
nodes[next] = n
|
|
1213
|
-
}
|
|
1214
|
-
for (const run of this._editorRuns) {
|
|
1215
|
-
const next = move(run.hostPath)
|
|
1216
|
-
if (next !== null) run.hostPath = next
|
|
1217
|
-
}
|
|
1218
|
-
for (const [ key, card ] of [ ...this._inspectorCards ]) {
|
|
1219
|
-
const i = key.lastIndexOf(":")
|
|
1220
|
-
const next = move(key.slice(0, i))
|
|
1221
|
-
if (next === null) continue
|
|
1222
|
-
this._inspectorCards.delete(key)
|
|
1223
|
-
this._inspectorCards.set(next + key.slice(i), card)
|
|
1224
|
-
}
|
|
1225
|
-
this._refreshRunDeps()
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
/** @internal Editor: rename ONE node (bare sibling segment — the subtree's paths follow).
|
|
1229
|
-
* Owns the shared record's re-keying (the host re-keys only its own part/selection state).
|
|
1230
|
-
* Returns the new path; null on refusal (unknown node, invalid name, sibling collision). */
|
|
1231
|
-
_renameNode(path: string, newName: string): string | null {
|
|
1232
|
-
const nodes = this._live?.nodes
|
|
1233
|
-
const node = nodes?.[path]
|
|
1234
|
-
if (!nodes || !node || newName === "" || newName.includes("/") || newName.includes(":")) return null
|
|
1235
|
-
const cut = path.lastIndexOf("/")
|
|
1236
|
-
const newPath = cut < 0 ? newName : path.slice(0, cut + 1) + newName
|
|
1237
|
-
if (newPath === path) return path
|
|
1238
|
-
if (nodes[newPath]) return null
|
|
1239
|
-
this._rekey(path, newPath)
|
|
1240
|
-
node.name = newName // engine-side name stays the bare segment
|
|
1241
|
-
return newPath
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/** @internal Editor: reparent keeping the LOCAL transform (null = scene root) — the node's and
|
|
1245
|
-
* every descendant's paths follow. Returns the new path; null on refusal (unknown node/parent,
|
|
1246
|
-
* cycle, name taken among the new siblings). */
|
|
1247
|
-
_reparentNode(path: string, newParentPath: string | null): string | null {
|
|
1248
|
-
const nodes = this._live?.nodes
|
|
1249
|
-
const node = nodes?.[path]
|
|
1250
|
-
if (!nodes || !node) return null
|
|
1251
|
-
const parent = newParentPath === null ? null : nodes[newParentPath]
|
|
1252
|
-
if (newParentPath !== null && !parent) return null
|
|
1253
|
-
if (newParentPath !== null && (newParentPath === path || newParentPath.startsWith(path + "/"))) return null
|
|
1254
|
-
const name = path.slice(path.lastIndexOf("/") + 1)
|
|
1255
|
-
const newPath = newParentPath === null ? name : `${newParentPath}/${name}`
|
|
1256
|
-
if (newPath === path) return path
|
|
1257
|
-
if (nodes[newPath]) return null
|
|
1258
|
-
this._rekey(path, newPath)
|
|
1259
|
-
node.setParent(parent ?? null, false)
|
|
1260
|
-
return newPath
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
/** @internal Editor: a node changed (transform edit / gizmo drag / live patch) — re-resolve refs
|
|
1264
|
-
* and re-run rebuild() on every editor-run aspect whose ref() props point at it. Deps are
|
|
1265
|
-
* absolute paths, so "the change counts for its ancestors too" (generators read subtrees —
|
|
1266
|
-
* FollowPath's waypoints are the children of its referenced path node) is a prefix test:
|
|
1267
|
-
* a dep hits when the changed path IS the dep or lies inside the dep's subtree. */
|
|
1268
|
-
_editorNodeChanged(path: string): void {
|
|
1269
|
-
const nodes = this._live?.nodes
|
|
1270
|
-
if (!nodes) return
|
|
1271
|
-
for (const run of this._editorRuns) {
|
|
1272
|
-
let hit = false
|
|
1273
|
-
for (const d of run.deps) if (path === d || path.startsWith(`${d}/`)) { hit = true; break }
|
|
1274
|
-
if (!hit) continue
|
|
1275
|
-
assignRefProps(run, nodes) // a patch may have replaced the referenced node instance
|
|
1276
|
-
safeRebuild(run)
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
/** @internal Editor: live arg edit on a make() node — re-calls the factory through the tracked
|
|
1281
|
-
* run (no compile; the factory is already in the bundle). False for non-make nodes. */
|
|
1282
|
-
_editorSetMakeArg(hostPath: string, key: string, value: unknown): boolean {
|
|
1283
|
-
return this._editorSetProp(hostPath, MAKE_INDEX, key, value)
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
/** @internal Editor: live prop edit on ONE editor-run aspect (`index` = the doc's aspect index
|
|
1287
|
-
* on the host node) — updates the instance (`{ $ref }` values resolve to live nodes), re-derives
|
|
1288
|
-
* its deps, and rebuilds. False when that entry isn't editor-run (inert data — nothing to do). */
|
|
1289
|
-
_editorSetProp(hostPath: string, index: number, key: string, value: unknown): boolean {
|
|
1290
|
-
const run = this._editorRuns.find((r) => r.hostPath === hostPath && r.index === index)
|
|
1291
|
-
const nodes = this._live?.nodes
|
|
1292
|
-
if (!run || !nodes) return false
|
|
1293
|
-
run.props[key] = value
|
|
1294
|
-
run.deps = collectRefDeps(run.props, nodes, run.hostPath)
|
|
1295
|
-
// aspect runs keep their host as a dep (see attachAspects); make() runs must NOT — the
|
|
1296
|
-
// wrapper's transform is editor-owned and moving it never re-calls the factory
|
|
1297
|
-
if (run.index !== MAKE_INDEX) run.deps.add(run.hostPath)
|
|
1298
|
-
if (isNodeRef(value) || (Array.isArray(value) && value.some(isNodeRef))) assignRefProps(run, nodes)
|
|
1299
|
-
else (run.inst as Record<string, unknown>)[key] = value
|
|
1300
|
-
safeRebuild(run)
|
|
1301
|
-
return true
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
* @internal Editor: run an aspect's custom `static inspector` card (immediate-mode — see
|
|
1306
|
-
* core/InspectorUI.ts) and return its widget list. Null when the class has no inspector (the
|
|
1307
|
-
* editor falls back to the inferred fields).
|
|
1308
|
-
*
|
|
1309
|
-
* `props` is the entry's CURRENT doc props, passed on EVERY call — the world syncs its side
|
|
1310
|
-
* from it (a generator syncs through the `_editorSetProp` machinery, so an undo that changes a
|
|
1311
|
-
* prop rebuilds for free; a plain aspect's preview instance is reassigned). `event` carries only
|
|
1312
|
-
* buttons and editor-state field edits — doc-bound field edits arrive as changed `props`.
|
|
1313
|
-
*
|
|
1314
|
-
* Cards persist per `<host>:<index>` across calls (that's where `ui.state` lives); a card whose
|
|
1315
|
-
* node instance was replaced by a live patch is rebuilt transparently.
|
|
1316
|
-
*/
|
|
1317
|
-
_inspectorRender(
|
|
1318
|
-
hostPath: string, index: number,
|
|
1319
|
-
props: Record<string, unknown>, event?: InspectorEvent,
|
|
1320
|
-
): InspectorWidget[] | null {
|
|
1321
|
-
const live = this._live
|
|
1322
|
-
const node = live?.nodes[hostPath]
|
|
1323
|
-
const entry = (node as unknown as { _sceneAspects?: readonly AspectEntry<any>[] } | undefined)
|
|
1324
|
-
?._sceneAspects?.[index]
|
|
1325
|
-
if (!live || !node || !entry) return null
|
|
1326
|
-
const ctor = entry.ctor as unknown as { inspector?: (ui: InspectorUI, aspect: unknown) => void }
|
|
1327
|
-
if (typeof ctor.inspector !== "function") return null
|
|
1328
|
-
|
|
1329
|
-
const run = this._editorRuns.find((r) => r.hostPath === hostPath && r.index === index)
|
|
1330
|
-
const key = `${hostPath}:${index}`
|
|
1331
|
-
let card = this._inspectorCards.get(key)
|
|
1332
|
-
if (!card || card.node !== node) {
|
|
1333
|
-
// (Re)create — generators reuse their tracked live instance; plain aspects get a persistent
|
|
1334
|
-
// preview instance: node set, refs resolved, NEVER onAttach (edit mode stays side-effect
|
|
1335
|
-
// free). Editor state (ui._state) survives a node patch by carrying the old ui over.
|
|
1336
|
-
let inst: Record<string, unknown>
|
|
1337
|
-
if (run) {
|
|
1338
|
-
inst = run.inst as unknown as Record<string, unknown>
|
|
1339
|
-
} else {
|
|
1340
|
-
inst = new (entry.ctor as unknown as new () => Record<string, unknown>)()
|
|
1341
|
-
inst.node = node
|
|
1342
|
-
Object.assign(inst, resolveRefs({ ...props }, live.nodes, hostPath))
|
|
1343
|
-
}
|
|
1344
|
-
const ui = card?.ui ?? new InspectorUI()
|
|
1345
|
-
ui._fields = describeFields(entry.ctor as unknown as abstract new () => unknown)
|
|
1346
|
-
ui._docKeys = new Set(ui._fields.map((f) => f.key))
|
|
1347
|
-
card = { ui, inst, node }
|
|
1348
|
-
this._inspectorCards.set(key, card)
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Sync the doc props into the world side. A key REMOVED from the doc (undo past its first
|
|
1352
|
-
// edit) resets to the class-field default — otherwise the instance would keep the stale value.
|
|
1353
|
-
const c = card
|
|
1354
|
-
const fieldDefault = (k: string): unknown => c.ui._fields.find((f) => f.key === k)?.value
|
|
1355
|
-
// `$expr` markers (values set in code) never sync into instances — the widget shows them
|
|
1356
|
-
// read-only; the instance keeps the compile-time evaluation.
|
|
1357
|
-
const isExpr = (v: unknown): boolean =>
|
|
1358
|
-
typeof v === "object" && v !== null && typeof (v as { $expr?: unknown }).$expr === "string"
|
|
1359
|
-
const changed = (a: unknown, b: unknown): boolean =>
|
|
1360
|
-
a !== b && JSON.stringify(a) !== JSON.stringify(b)
|
|
1361
|
-
if (run) {
|
|
1362
|
-
for (const [ k, v ] of Object.entries(props)) {
|
|
1363
|
-
if (!isExpr(v) && changed(run.props[k], v)) this._editorSetProp(hostPath, index, k, v)
|
|
1364
|
-
}
|
|
1365
|
-
for (const k of Object.keys(run.props)) {
|
|
1366
|
-
if (k in props) continue
|
|
1367
|
-
this._editorSetProp(hostPath, index, k, fieldDefault(k))
|
|
1368
|
-
delete run.props[k] // keep run.props mirroring the doc, or this reset re-fires every call
|
|
1369
|
-
}
|
|
1370
|
-
c.ui._props = run.props
|
|
1371
|
-
} else {
|
|
1372
|
-
const snapshot = { ...props }
|
|
1373
|
-
const resolved = (resolveRefs(snapshot, live.nodes, hostPath) ?? snapshot) as Record<string, unknown>
|
|
1374
|
-
for (const f of c.ui._fields) {
|
|
1375
|
-
if (isExpr(resolved[f.key])) continue
|
|
1376
|
-
c.inst[f.key] = f.key in resolved ? resolved[f.key] : f.value
|
|
1377
|
-
}
|
|
1378
|
-
c.ui._props = snapshot
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
return c.ui._run((u) => ctor.inspector!(u, c.inst), event)
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
/** @internal Editor: the INTERNAL part rows of a loaded model node or prefab instance (by its
|
|
1385
|
-
* absolute def path) — path/name/depth/live node, in the same asset-internal part-path grammar
|
|
1386
|
-
* `overrides` keys use. Empty for plain nodes / unknown paths. */
|
|
1387
|
-
async _modelParts(path: string): Promise<ModelPartRow[]> {
|
|
1388
|
-
const { nodes } = (await this.load()) as unknown as { nodes: Record<string, Node> }
|
|
1389
|
-
const node = nodes[path]
|
|
1390
|
-
if (node instanceof Model) return modelPartRows(node)
|
|
1391
|
-
// prefab instances precompute their rows in DEF order (engine child order isn't stable)
|
|
1392
|
-
return (node as { _prefabParts?: ModelPartRow[] } | undefined)?._prefabParts ?? []
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
/** @internal Editor: describe every aspect class this scene references (fields + defaults). */
|
|
1396
|
-
_describeAspects(): AspectClassInfo[] {
|
|
1397
|
-
const ctors = new Set<AspectCtor<any>>()
|
|
1398
|
-
const walk = (defs?: Record<string, SceneNodeDef>): void => {
|
|
1399
|
-
for (const nd of Object.values(defs ?? {})) {
|
|
1400
|
-
for (const e of nd.aspects ?? []) ctors.add(e.ctor)
|
|
1401
|
-
walk(nd.children)
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
walk(this.def.nodes)
|
|
1405
|
-
return [ ...ctors ].map((c) => describeAspect(c))
|
|
1406
|
-
}
|
|
1407
1103
|
}
|
|
1408
1104
|
|
|
1409
1105
|
/**
|
|
@@ -1417,6 +1113,6 @@ export const defineScene = <const D extends SceneDef>(def: D): SceneHandle<D> =>
|
|
|
1417
1113
|
const g = globalThis as unknown as { [EDIT_FLAG]?: boolean, __lecodesScenes?: SceneHandle[] }
|
|
1418
1114
|
// Editor hook: expose defined handles to the host (the scene editor runs the bundle, then picks
|
|
1419
1115
|
// up the handle to load it in edit mode and drive the inspector).
|
|
1420
|
-
if (g[EDIT_FLAG]) (g.__lecodesScenes ??= []).push(handle as SceneHandle)
|
|
1116
|
+
if (EDITOR && g[EDIT_FLAG]) (g.__lecodesScenes ??= []).push(handle as SceneHandle)
|
|
1421
1117
|
return handle
|
|
1422
1118
|
}
|