Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.
Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.
Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.
Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).
Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.
Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.
Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.
VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.
Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.
7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
359 lines
8.0 KiB
Go
359 lines
8.0 KiB
Go
package view
|
|
|
|
import (
|
|
"aego/core/mathx"
|
|
"aego/gpu"
|
|
)
|
|
|
|
type Camera2D struct {
|
|
Position mathx.Vec2
|
|
Rotation float32
|
|
Zoom float32
|
|
}
|
|
|
|
type ScaleMode uint8
|
|
|
|
const (
|
|
ScaleFree ScaleMode = iota
|
|
ScalePixelPerfect
|
|
ScaleFitVirtual
|
|
)
|
|
|
|
type Projection uint8
|
|
|
|
const (
|
|
Ortho2D Projection = iota
|
|
Perspective3D
|
|
Ortho3D
|
|
)
|
|
|
|
type LayerMask uint32
|
|
|
|
const AllLayers LayerMask = 0xffffffff
|
|
|
|
const (
|
|
DefaultFOV float32 = mathx.Pi / 3
|
|
DefaultNear float32 = 0.1
|
|
DefaultFar float32 = 1000
|
|
DefaultOrthoSize float32 = 5
|
|
)
|
|
|
|
type View struct {
|
|
Projection Projection
|
|
Camera Camera2D
|
|
Eye mathx.Mat4
|
|
FOV float32
|
|
OrthoSize float32
|
|
Near, Far float32
|
|
Viewport mathx.Rect
|
|
Target gpu.TextureHandle
|
|
DepthTarget gpu.TextureHandle
|
|
TargetW int
|
|
TargetH int
|
|
PixelScale float32
|
|
PixelsPerUnit float32
|
|
ZMin, ZMax float32
|
|
Clear mathx.Color
|
|
DoClear bool
|
|
Layers LayerMask
|
|
Scale ScaleMode
|
|
Virtual mathx.Vec2
|
|
Label string
|
|
}
|
|
|
|
func ScreenView(pixelW, pixelH int, pixelScale float32) View {
|
|
if pixelScale <= 0 {
|
|
pixelScale = 1
|
|
}
|
|
lw := float32(pixelW) / pixelScale
|
|
lh := float32(pixelH) / pixelScale
|
|
return View{
|
|
Camera: Camera2D{Position: mathx.Vec2{X: lw / 2, Y: lh / 2}, Zoom: pixelScale},
|
|
Viewport: mathx.Rect{W: float32(pixelW), H: float32(pixelH)},
|
|
TargetW: pixelW,
|
|
TargetH: pixelH,
|
|
PixelScale: pixelScale,
|
|
PixelsPerUnit: 1,
|
|
ZMax: 1,
|
|
Layers: AllLayers,
|
|
Label: "screen",
|
|
}
|
|
}
|
|
|
|
func PerspectiveView(eye mathx.Mat4, fov, near, far float32, vp mathx.Rect) View {
|
|
return View{
|
|
Projection: Perspective3D,
|
|
Camera: Camera2D{Zoom: 1},
|
|
Eye: eye,
|
|
FOV: fov,
|
|
Near: near,
|
|
Far: far,
|
|
Viewport: vp,
|
|
PixelScale: 1,
|
|
PixelsPerUnit: 1,
|
|
ZMax: 1,
|
|
Layers: AllLayers,
|
|
Label: "perspective",
|
|
}
|
|
}
|
|
|
|
func OrthoView(eye mathx.Mat4, size, near, far float32, vp mathx.Rect) View {
|
|
v := PerspectiveView(eye, 0, near, far, vp)
|
|
v.Projection = Ortho3D
|
|
v.OrthoSize = size
|
|
v.Label = "ortho"
|
|
return v
|
|
}
|
|
|
|
func (v View) Is3D() bool {
|
|
return v.Projection == Perspective3D || v.Projection == Ortho3D
|
|
}
|
|
|
|
func (v *View) Normalize(pixelW, pixelH int, pixelScale float32) {
|
|
if v.TargetW <= 0 || v.TargetH <= 0 {
|
|
v.TargetW, v.TargetH = pixelW, pixelH
|
|
}
|
|
if v.Viewport.W <= 0 || v.Viewport.H <= 0 {
|
|
v.Viewport = mathx.Rect{W: float32(v.TargetW), H: float32(v.TargetH)}
|
|
}
|
|
if v.PixelScale <= 0 {
|
|
v.PixelScale = pixelScale
|
|
}
|
|
if v.PixelScale <= 0 {
|
|
v.PixelScale = 1
|
|
}
|
|
if v.PixelsPerUnit <= 0 {
|
|
v.PixelsPerUnit = 1
|
|
}
|
|
if v.Camera.Zoom <= 0 {
|
|
v.Camera.Zoom = 1
|
|
}
|
|
if v.Layers == 0 {
|
|
v.Layers = AllLayers
|
|
}
|
|
if v.ZMax <= v.ZMin {
|
|
v.ZMin, v.ZMax = 0, 1
|
|
}
|
|
if !v.Is3D() {
|
|
return
|
|
}
|
|
if v.Eye == (mathx.Mat4{}) {
|
|
v.Eye = mathx.Mat4Identity()
|
|
}
|
|
if v.Projection == Ortho3D {
|
|
if v.OrthoSize <= 0 {
|
|
v.OrthoSize = DefaultOrthoSize
|
|
}
|
|
if v.Far <= v.Near {
|
|
v.Near, v.Far = DefaultNear, DefaultFar
|
|
}
|
|
return
|
|
}
|
|
if v.FOV <= 0 {
|
|
v.FOV = DefaultFOV
|
|
}
|
|
if v.Near <= 0 {
|
|
v.Near = DefaultNear
|
|
}
|
|
if v.Far <= v.Near {
|
|
v.Far = max(DefaultFar, v.Near*2)
|
|
}
|
|
}
|
|
|
|
func (v View) UsesDepth() bool {
|
|
return v.Is3D() && (v.Target.IsZero() || !v.DepthTarget.IsZero())
|
|
}
|
|
|
|
func (v View) Flipped(caps gpu.Capabilities) bool {
|
|
return !v.Target.IsZero() && caps.OriginBottomLeft
|
|
}
|
|
|
|
func (v View) EffectiveZoom() float32 {
|
|
z := v.Camera.Zoom
|
|
if z <= 0 {
|
|
z = 1
|
|
}
|
|
switch v.Scale {
|
|
case ScaleFitVirtual:
|
|
return z * v.virtualFit()
|
|
case ScalePixelPerfect:
|
|
f := float32(int(v.virtualFit()))
|
|
if f < 1 {
|
|
f = 1
|
|
}
|
|
return z * f
|
|
}
|
|
return z
|
|
}
|
|
|
|
func (v View) virtualFit() float32 {
|
|
if v.Virtual.X <= 0 || v.Virtual.Y <= 0 {
|
|
return 1
|
|
}
|
|
return min(v.Viewport.W/v.Virtual.X, v.Viewport.H/v.Virtual.Y)
|
|
}
|
|
|
|
func (v View) LetterboxViewport() mathx.Rect {
|
|
if v.Is3D() || v.Scale == ScaleFree || v.Virtual.X <= 0 || v.Virtual.Y <= 0 {
|
|
return v.Viewport
|
|
}
|
|
zoom := v.EffectiveZoom() / max(v.Camera.Zoom, 0.0001)
|
|
w := v.Virtual.X * zoom
|
|
h := v.Virtual.Y * zoom
|
|
return mathx.Rect{
|
|
X: v.Viewport.X + (v.Viewport.W-w)/2,
|
|
Y: v.Viewport.Y + (v.Viewport.H-h)/2,
|
|
W: w,
|
|
H: h,
|
|
}
|
|
}
|
|
|
|
func (v View) cameraPosition() mathx.Vec2 {
|
|
p := v.Camera.Position
|
|
if v.Scale != ScalePixelPerfect {
|
|
return p
|
|
}
|
|
z := v.EffectiveZoom()
|
|
if z <= 0 {
|
|
return p
|
|
}
|
|
return mathx.Vec2{
|
|
X: float32(int(p.X*z+0.5)) / z,
|
|
Y: float32(int(p.Y*z+0.5)) / z,
|
|
}
|
|
}
|
|
|
|
func (v View) CameraPosition() mathx.Vec3 {
|
|
if v.Is3D() {
|
|
return v.Eye.Translation()
|
|
}
|
|
p := v.cameraPosition()
|
|
return mathx.Vec3{X: p.X, Y: p.Y}
|
|
}
|
|
|
|
func (v View) ViewMatrix() mathx.Mat3 {
|
|
z := v.EffectiveZoom()
|
|
pos := v.cameraPosition()
|
|
translate := mathx.Mat3TRS(pos.Neg(), 0, mathx.Vec2{X: 1, Y: 1})
|
|
rotate := mathx.Mat3TRS(mathx.Vec2{}, -v.Camera.Rotation, mathx.Vec2{X: z, Y: z})
|
|
return rotate.Mul(translate)
|
|
}
|
|
|
|
func (v View) ViewMatrix4() mathx.Mat4 {
|
|
if v.Is3D() {
|
|
if inv, ok := v.Eye.InverseAffine(); ok {
|
|
return inv
|
|
}
|
|
return mathx.Mat4Identity()
|
|
}
|
|
return mathx.Mat4FromMat3(v.ViewMatrix())
|
|
}
|
|
|
|
func (v View) glProjection() mathx.Mat4 {
|
|
vp := v.LetterboxViewport()
|
|
aspect := float32(1)
|
|
if vp.H > 0 {
|
|
aspect = vp.W / vp.H
|
|
}
|
|
switch v.Projection {
|
|
case Perspective3D:
|
|
return mathx.Mat4Perspective(v.FOV, aspect, v.Near, v.Far)
|
|
case Ortho3D:
|
|
hh := v.OrthoSize
|
|
hw := hh * aspect
|
|
return mathx.Mat4Ortho(-hw, hw, -hh, hh, v.Near, v.Far)
|
|
}
|
|
hw, hh := vp.W/2, vp.H/2
|
|
return mathx.Mat4Ortho(-hw, hw, hh, -hh, -1, 1)
|
|
}
|
|
|
|
func (v View) ProjectionMatrix(caps gpu.Capabilities) mathx.Mat4 {
|
|
p := v.glProjection()
|
|
if v.Flipped(caps) {
|
|
flip := mathx.Mat4Identity()
|
|
flip[5] = -1
|
|
p = flip.Mul(p)
|
|
}
|
|
if caps.ClipZeroToOne {
|
|
fix := mathx.Mat4Identity()
|
|
fix[10] = 0.5
|
|
fix[14] = 0.5
|
|
p = fix.Mul(p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (v View) ViewProjection(caps gpu.Capabilities) mathx.Mat4 {
|
|
return v.ProjectionMatrix(caps).Mul(v.ViewMatrix4())
|
|
}
|
|
|
|
func (v View) ViewProj(caps gpu.Capabilities) mathx.Mat4 {
|
|
return v.ViewProjection(caps)
|
|
}
|
|
|
|
func (v View) clipFromWorld() mathx.Mat4 {
|
|
return v.glProjection().Mul(v.ViewMatrix4())
|
|
}
|
|
|
|
func (v View) Frustum() mathx.Frustum {
|
|
return mathx.FrustumFromMat4(v.clipFromWorld())
|
|
}
|
|
|
|
func (v View) ScreenToRay(p mathx.Vec2) mathx.Ray3 {
|
|
vp := v.LetterboxViewport()
|
|
if vp.W <= 0 || vp.H <= 0 {
|
|
return mathx.Ray3{}
|
|
}
|
|
nx := (p.X-vp.X)/vp.W*2 - 1
|
|
ny := 1 - (p.Y-vp.Y)/vp.H*2
|
|
inv, ok := v.clipFromWorld().Inverse()
|
|
if !ok {
|
|
return mathx.Ray3{}
|
|
}
|
|
a := inv.TransformPoint(mathx.Vec3{X: nx, Y: ny, Z: -1})
|
|
b := inv.TransformPoint(mathx.Vec3{X: nx, Y: ny, Z: 1})
|
|
return mathx.Ray3{Origin: a, Dir: b.Sub(a).Normalized()}
|
|
}
|
|
|
|
func (v View) WorldToScreen3(p mathx.Vec3) (mathx.Vec2, bool) {
|
|
c := v.clipFromWorld().MulVec4(mathx.Vec4{X: p.X, Y: p.Y, Z: p.Z, W: 1})
|
|
if c.W <= 1e-6 {
|
|
return mathx.Vec2{}, false
|
|
}
|
|
vp := v.LetterboxViewport()
|
|
nx, ny := c.X/c.W, c.Y/c.W
|
|
return mathx.Vec2{X: vp.X + (nx+1)/2*vp.W, Y: vp.Y + (1-ny)/2*vp.H}, true
|
|
}
|
|
|
|
func (v View) WorldToScreen(p mathx.Vec2) mathx.Vec2 {
|
|
vp := v.LetterboxViewport()
|
|
local := v.ViewMatrix().Apply(p)
|
|
return mathx.Vec2{X: vp.X + vp.W/2 + local.X, Y: vp.Y + vp.H/2 + local.Y}
|
|
}
|
|
|
|
func (v View) ScreenToWorld(p mathx.Vec2) mathx.Vec2 {
|
|
vp := v.LetterboxViewport()
|
|
local := mathx.Vec2{X: p.X - vp.X - vp.W/2, Y: p.Y - vp.Y - vp.H/2}
|
|
inv, ok := v.ViewMatrix().Inverse()
|
|
if !ok {
|
|
return v.cameraPosition()
|
|
}
|
|
return inv.Apply(local)
|
|
}
|
|
|
|
func (v View) VisibleBounds() mathx.AABB2 {
|
|
if v.Is3D() {
|
|
inf := mathx.Inf()
|
|
return mathx.AABB2{Min: mathx.Vec2{X: -inf, Y: -inf}, Max: mathx.Vec2{X: inf, Y: inf}}
|
|
}
|
|
vp := v.LetterboxViewport()
|
|
c0 := v.ScreenToWorld(mathx.Vec2{X: vp.X, Y: vp.Y})
|
|
c1 := v.ScreenToWorld(mathx.Vec2{X: vp.X + vp.W, Y: vp.Y})
|
|
c2 := v.ScreenToWorld(mathx.Vec2{X: vp.X, Y: vp.Y + vp.H})
|
|
c3 := v.ScreenToWorld(mathx.Vec2{X: vp.X + vp.W, Y: vp.Y + vp.H})
|
|
return mathx.AABB2{
|
|
Min: mathx.Vec2{X: min(min(c0.X, c1.X), min(c2.X, c3.X)), Y: min(min(c0.Y, c1.Y), min(c2.Y, c3.Y))},
|
|
Max: mathx.Vec2{X: max(max(c0.X, c1.X), max(c2.X, c3.X)), Y: max(max(c0.Y, c1.Y), max(c2.Y, c3.Y))},
|
|
}
|
|
}
|