Skip to content

Commit 46bd2c3

Browse files
authored
Merge pull request #17 from gaarutyunov/claude/fix-canvas-cube-controls-01QrkYYrKSL6AahZgmtozD8S
fix: Make canvas square to prevent cube distortion
2 parents 754c6ee + 7ee1e6d commit 46bd2c3

10 files changed

Lines changed: 161 additions & 39 deletions

File tree

examples/webgpu-cube/app.gx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import "fmt"
4+
import "github.com/gaarutyunov/guix/pkg/runtime"
45

56
func App() (Component) {
67
rotationX := 0.0
@@ -9,6 +10,8 @@ func App() (Component) {
910
speed := 1.0
1011
commands := make(chan ControlCommand, 10)
1112
controlState := make(chan ControlState, 10)
13+
rotXPtr := &rotationX
14+
rotYPtr := &rotationY
1215

1316
go func() {
1417
state := ControlState{AutoRotate: autoRotate, Speed: float32(speed)}
@@ -60,10 +63,10 @@ func App() (Component) {
6063
Canvas(
6164
ID("webgpu-canvas"),
6265
Width(500),
63-
Height(375),
66+
Height(500),
6467
GPURenderUpdate(renderUpdate)
6568
) {
66-
GPUScene(NewCubeScene(float32(rotationX), float32(rotationY)))
69+
GPUScene(NewCubeScene(rotXPtr, rotYPtr))
6770
}
6871
Controls(WithCommands(commands), WithState(controlState))
6972
}

examples/webgpu-cube/app_gen.go

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/webgpu-cube/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
display: block;
4545
border-radius: 8px;
4646
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
47-
max-width: 100%;
47+
width: 100%;
48+
max-width: 500px;
49+
aspect-ratio: 1 / 1;
4850
height: auto;
4951
}
5052

examples/webgpu-cube/scene.gx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package main
22

3-
func CubeScene(rotX float32, rotY float32) (Scene) {
3+
func CubeScene(rotX *float64, rotY *float64) (Scene) {
4+
rotZ := 0.0
5+
46
Scene(Background(0.1, 0.1, 0.15, 1.0)) {
57
Mesh(
6-
GeometryProp(NewBoxGeometry(2.0, 2.0, 2.0)),
7-
MaterialProp(StandardMaterial(
8+
WithGeometry(NewBoxGeometry(2.0, 2.0, 2.0)),
9+
WithMaterial(StandardMaterial(
810
Color(0.91, 0.27, 0.38, 1.0),
911
Metalness(0.3),
1012
Roughness(0.4)
1113
)),
1214
Position(0, 0, 0),
13-
Rotation(rotX, rotY, 0),
15+
BindRotation(rotX, rotY, &rotZ),
1416
ScaleValue(1, 1, 1)
1517
)
1618

examples/webgpu-cube/scene_gen.go

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

magefile.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,44 @@ func Vet() error {
3939
return sh.RunWith(env, "go", "vet", "./pkg/runtime/...")
4040
}
4141

42-
// Test runs all tests
42+
// Test runs all tests (excluding WASM-only packages)
4343
func Test() error {
4444
fmt.Println("Running tests...")
45-
return sh.RunV("go", "test", "./...")
45+
// Test non-WASM packages (exclude runtime which requires WASM runtime to execute)
46+
packages := []string{
47+
"./cmd/...",
48+
"./internal/...",
49+
"./pkg/ast",
50+
"./pkg/codegen",
51+
"./pkg/parser",
52+
"./pkg/visitors",
53+
}
54+
for _, pkg := range packages {
55+
if err := sh.RunV("go", "test", pkg); err != nil {
56+
return err
57+
}
58+
}
59+
return nil
4660
}
4761

48-
// Build builds all packages for native target
62+
// Build builds all packages for native target (excluding WASM-only packages)
4963
func Build() error {
5064
fmt.Println("Building native packages...")
51-
return sh.RunV("go", "build", "./...")
65+
// Build non-WASM packages (exclude runtime which is WASM-only)
66+
packages := []string{
67+
"./cmd/...",
68+
"./internal/...",
69+
"./pkg/ast",
70+
"./pkg/codegen",
71+
"./pkg/parser",
72+
"./pkg/visitors",
73+
}
74+
for _, pkg := range packages {
75+
if err := sh.RunV("go", "build", pkg); err != nil {
76+
return err
77+
}
78+
}
79+
return nil
5280
}
5381

5482
// BuildWasm builds the runtime package for WebAssembly

pkg/ast/ast.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ type Type struct {
8686
IsChannel bool `@("<-")?`
8787
IsChan bool `@("chan")?`
8888
IsSlice bool `@("[" "]")?`
89+
IsPointer bool `@("*")?`
8990
IsInterface bool `@("interface" "{" "}")?` // Empty interface type
9091
Name string `@Ident?`
9192
Generic *Type `("[" @@ "]")?`
92-
IsPointer bool `@("*")?`
9393
IsFunc bool `@("func")?`
9494
FuncParams []*Type
9595
FuncResults []*Type
@@ -219,7 +219,7 @@ type KeyValue struct {
219219
// UnaryExpr represents a unary expression (e.g., !x, -x)
220220
type UnaryExpr struct {
221221
Pos lexer.Position
222-
Op string `@("!" | "-" | "+")`
222+
Op string `@("!" | "-" | "+" | "&" | "*")`
223223
Right *Primary `@@`
224224
}
225225

pkg/codegen/codegen.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,27 @@ func (g *Generator) generateSceneConstructor(comp *guixast.Component) *ast.FuncD
745745

746746
// generateRenderSceneMethod generates the RenderScene method for a Scene component
747747
func (g *Generator) generateRenderSceneMethod(comp *guixast.Component) *ast.FuncDecl {
748+
// Generate variable declarations first
749+
var stmts []ast.Stmt
750+
if comp.Body != nil && len(comp.Body.VarDecls) > 0 {
751+
for _, varDecl := range comp.Body.VarDecls {
752+
// Generate: varName := value
753+
var lhs []ast.Expr
754+
for _, name := range varDecl.Names {
755+
lhs = append(lhs, ast.NewIdent(name))
756+
}
757+
var rhs []ast.Expr
758+
for _, value := range varDecl.Values {
759+
rhs = append(rhs, g.generateExpr(value))
760+
}
761+
stmts = append(stmts, &ast.AssignStmt{
762+
Lhs: lhs,
763+
Tok: token.DEFINE, // :=
764+
Rhs: rhs,
765+
})
766+
}
767+
}
768+
748769
// Generate scene graph from body children
749770
var sceneNode ast.Expr
750771
if comp.Body != nil && len(comp.Body.Children) > 0 {
@@ -760,6 +781,11 @@ func (g *Generator) generateRenderSceneMethod(comp *guixast.Component) *ast.Func
760781
}
761782
}
762783

784+
// Add return statement
785+
stmts = append(stmts, &ast.ReturnStmt{
786+
Results: []ast.Expr{sceneNode},
787+
})
788+
763789
return &ast.FuncDecl{
764790
Recv: &ast.FieldList{
765791
List: []*ast.Field{
@@ -787,11 +813,7 @@ func (g *Generator) generateRenderSceneMethod(comp *guixast.Component) *ast.Func
787813
},
788814
},
789815
Body: &ast.BlockStmt{
790-
List: []ast.Stmt{
791-
&ast.ReturnStmt{
792-
Results: []ast.Expr{sceneNode},
793-
},
794-
},
816+
List: stmts,
795817
},
796818
}
797819
}
@@ -1867,6 +1889,7 @@ var runtimeFunctions = map[string]bool{
18671889
"LookAtPos": true, "Background": true,
18681890
"Width": true, "Height": true,
18691891
"GeometryProp": true, "MaterialProp": true, "GPURenderUpdate": true,
1892+
"WithGeometry": true, "WithMaterial": true, "BindRotation": true,
18701893
// GPU constructors
18711894
"NewBoxGeometry": true, "NewSphereGeometry": true, "NewPlaneGeometry": true,
18721895
"StandardMaterial": true,
@@ -2428,6 +2451,10 @@ func (g *Generator) unaryOpToToken(op string) token.Token {
24282451
return token.SUB
24292452
case "+":
24302453
return token.ADD
2454+
case "&":
2455+
return token.AND
2456+
case "*":
2457+
return token.MUL
24312458
default:
24322459
return token.NOT // Default fallback
24332460
}

pkg/runtime/gpu_renderer.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ type SceneRenderer struct {
2020
AmbientLight *Light
2121
}
2222

23+
// ReactiveBinding holds pointers to values that should be synced to transform
24+
type ReactiveBinding struct {
25+
RotationX *float64
26+
RotationY *float64
27+
RotationZ *float64
28+
}
29+
2330
// MeshInstance represents an instantiated mesh with buffers
2431
type MeshInstance struct {
25-
Transform Transform
26-
Geometry Geometry
27-
Material *Material
28-
VertexBuffer *GPUBuffer
29-
IndexBuffer *GPUBuffer
30-
IndexCount int
32+
Transform Transform
33+
Geometry Geometry
34+
Material *Material
35+
VertexBuffer *GPUBuffer
36+
IndexBuffer *GPUBuffer
37+
IndexCount int
38+
ReactiveBinding *ReactiveBinding // Reactive binding for auto-updates
3139
}
3240

3341
// NewSceneRenderer creates a new scene renderer
@@ -160,13 +168,22 @@ func (sr *SceneRenderer) createMeshInstance(node *GPUNode) (*MeshInstance, error
160168
}
161169
}
162170

171+
// Check for reactive binding
172+
var binding *ReactiveBinding
173+
if node.Properties != nil {
174+
if b, ok := node.Properties["bindRotation"].(*ReactiveBinding); ok {
175+
binding = b
176+
}
177+
}
178+
163179
return &MeshInstance{
164-
Transform: node.Transform,
165-
Geometry: node.Geometry,
166-
Material: material,
167-
VertexBuffer: vertexBuffer,
168-
IndexBuffer: indexBuffer,
169-
IndexCount: len(indices),
180+
Transform: node.Transform,
181+
Geometry: node.Geometry,
182+
Material: material,
183+
VertexBuffer: vertexBuffer,
184+
IndexBuffer: indexBuffer,
185+
IndexCount: len(indices),
186+
ReactiveBinding: binding,
170187
}, nil
171188
}
172189

@@ -227,8 +244,28 @@ func (sr *SceneRenderer) createPipeline() error {
227244
return nil
228245
}
229246

247+
// syncReactiveBindings updates mesh transforms from reactive bindings
248+
func (sr *SceneRenderer) syncReactiveBindings() {
249+
for _, mesh := range sr.Meshes {
250+
if binding := mesh.ReactiveBinding; binding != nil {
251+
if binding.RotationX != nil {
252+
mesh.Transform.Rotation.X = float32(*binding.RotationX)
253+
}
254+
if binding.RotationY != nil {
255+
mesh.Transform.Rotation.Y = float32(*binding.RotationY)
256+
}
257+
if binding.RotationZ != nil {
258+
mesh.Transform.Rotation.Z = float32(*binding.RotationZ)
259+
}
260+
}
261+
}
262+
}
263+
230264
// Render renders the scene
231265
func (sr *SceneRenderer) Render() {
266+
// Sync reactive bindings before rendering
267+
sr.syncReactiveBindings()
268+
232269
if sr.ActiveCamera == nil {
233270
logError("No active camera in scene")
234271
return

pkg/runtime/gpu_vnode.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,16 +156,36 @@ func OnGPURender(fn func(*GPUCanvas, float64)) GPUProp {
156156
return GPUProp{Key: "onRender", Value: fn}
157157
}
158158

159-
// GeometryProp wraps geometry for property passing
160-
func GeometryProp(geom Geometry) GPUProp {
159+
// WithGeometry wraps geometry for property passing
160+
func WithGeometry(geom Geometry) GPUProp {
161161
return GPUProp{Key: "geometry", Value: geom}
162162
}
163163

164-
// MaterialProp wraps material for property passing
165-
func MaterialProp(mat *Material) GPUProp {
164+
// GeometryProp is deprecated, use WithGeometry instead
165+
func GeometryProp(geom Geometry) GPUProp {
166+
return WithGeometry(geom)
167+
}
168+
169+
// WithMaterial wraps material for property passing
170+
func WithMaterial(mat *Material) GPUProp {
166171
return GPUProp{Key: "material", Value: mat}
167172
}
168173

174+
// MaterialProp is deprecated, use WithMaterial instead
175+
func MaterialProp(mat *Material) GPUProp {
176+
return WithMaterial(mat)
177+
}
178+
179+
// BindRotation creates a reactive binding for rotation values
180+
// The mesh will automatically update its rotation from these pointers each frame
181+
func BindRotation(x, y, z *float64) GPUProp {
182+
return GPUProp{Key: "bindRotation", Value: &ReactiveBinding{
183+
RotationX: x,
184+
RotationY: y,
185+
RotationZ: z,
186+
}}
187+
}
188+
169189
// GPU Node Builders
170190
// Note: Declarative GPU canvas builders are currently unused.
171191
// The working implementation uses CreateGPUCanvas() directly.

0 commit comments

Comments
 (0)