ianmackenzie / elm-3d-scene

A high-level 3D rendering engine for Elm, with support for lighting, shadows, and realistic materials.
https://package.elm-lang.org/packages/ianmackenzie/elm-3d-scene/latest/
Mozilla Public License 2.0
207 stars 27 forks source link

Cannot have a function that takes a model and returns (the Html element containing) a scene #105

Closed perkee closed 11 months ago

perkee commented 11 months ago

The Problem

Expected behavior:

Clicking the one button in this example should update the model and otherwise do nothing. The console should not have any errors.

Actual behavior

The console shows many copies of this error (1 per frame in a 60fps browser maybe?)

main.html:2843 Uncaught TypeError: Cannot read properties of undefined (reading 'length')
    at _VirtualDom_render (main.html:2843:43)
    at _VirtualDom_applyPatchRedraw (main.html:3878:16)
    at _VirtualDom_addDomNodesHelp (main.html:3728:5)
    at _VirtualDom_addDomNodesHelp (main.html:3643:5)
    at _VirtualDom_addDomNodesHelp (main.html:3739:8)
    at _VirtualDom_addDomNodes (main.html:3617:2)
    at _VirtualDom_applyPatches (main.html:3762:2)
    at main.html:4038:15
    at updateIfNeeded (main.html:4107:56)

The line in the stack trace points to kids.length in the last for loop in the code below. Lines included above just so it's easier to find in the generated JS. The error will be the same if I build with a JS file as output, only the line numbers will be different naturally.

    // at this point `tag` must be 1 or 2

    var domNode = vNode.f
        ? _VirtualDom_doc.createElementNS(vNode.f, vNode.c)
        : _VirtualDom_doc.createElement(vNode.c);
    domNode.created_by_elm = true;

    if (_VirtualDom_divertHrefToApp && vNode.c == 'a')
    {
        domNode.addEventListener('click', _VirtualDom_divertHrefToApp(domNode));
    }

    _VirtualDom_applyFacts(domNode, eventNode, vNode.d);

    for (var kids = vNode.e, i = 0; i < kids.length; i++) 
    {
        _VirtualDom_appendChild(domNode, _VirtualDom_render(tag === 1 ? kids[i] : kids[i].b, eventNode));
    }

    return domNode;

Reproducing the error

Platform

elm make src/Main.elm --output=main.html but this persists whether or not I have --debug or --optimize or --output=main.js then include into main.js into a handwritten index.html. It persists whether I open the HTML file in the browser (i.e. with file:///path/to/index.html) or if I serve it using a minimal webserver (I tend to use python3 -m http.server 4321). I have even reproduced it in a lamdera page.

elm.json

{
    "type": "application",
    "source-directories": [
        "src"
    ],
    "elm-version": "0.19.1",
    "dependencies": {
        "direct": {
            "avh4/elm-color": "1.0.0",
            "elm/browser": "1.0.2",
            "elm/core": "1.0.5",
            "elm/html": "1.0.0",
            "ianmackenzie/elm-3d-camera": "3.1.0",
            "ianmackenzie/elm-3d-scene": "1.0.1",
            "ianmackenzie/elm-geometry": "3.11.0",
            "ianmackenzie/elm-units": "2.10.0"
        },
        "indirect": {
            "elm/json": "1.1.3",
            "elm/random": "1.0.0",
            "elm/time": "1.0.0",
            "elm/url": "1.0.0",
            "elm/virtual-dom": "1.0.3",
            "elm-explorations/linear-algebra": "1.0.3",
            "elm-explorations/webgl": "1.1.3",
            "ianmackenzie/elm-1d-parameter": "1.0.1",
            "ianmackenzie/elm-float-extra": "1.1.0",
            "ianmackenzie/elm-geometry-linear-algebra-interop": "2.0.2",
            "ianmackenzie/elm-interval": "3.1.0",
            "ianmackenzie/elm-triangular-mesh": "1.1.0",
            "ianmackenzie/elm-units-interval": "3.2.0"
        }
    },
    "test-dependencies": {
        "direct": {},
        "indirect": {}
    }
}

src/Main.elm

module Main exposing (main)

import Angle
import Browser
import Camera3d
import Color
import Direction3d
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Length
import Pixels
import Point3d
import Scene3d
import Scene3d.Material as Material
import Viewpoint3d

type Model
    = Model

type Msg
    = MakeErrorsHappen

update : Msg -> Model -> ( Model, Cmd msg )
update MakeErrorsHappen _ =
    ( Model, Cmd.none )

main =
    Browser.element
        { init = \() -> ( Model, Cmd.none )
        , subscriptions = \Model -> Sub.none
        , update = update
        , view = view
        }

view : Model -> Html Msg
view model =
    Html.div
        []
        [ scene model
        , button [ onClick MakeErrorsHappen ]
            [ text "make bugs"
            ]
        ]

scene : Model -> Html msg
scene _ =
    let
        underlayment : Scene3d.Entity coordinates
        underlayment =
            Scene3d.quad (Material.color (Color.rgba 255 0 0 0.5))
                (Point3d.meters -5 -5 -1)
                (Point3d.meters 5 -5 -1)
                (Point3d.meters 5 5 -1)
                (Point3d.meters -5 5 -1)

        r =
            42.0

        ( camX, camY, camZ ) =
            ( r * cos 0.0, r * sin 0.0, 24.0 )

        camera =
            Camera3d.perspective
                { viewpoint =
                    Viewpoint3d.lookAt
                        { focalPoint = Point3d.origin
                        , eyePoint = Point3d.meters camX camY camZ
                        , upDirection = Direction3d.positiveZ
                        }
                , verticalFieldOfView = Angle.degrees 30
                }
    in
    Scene3d.cloudy
        { entities = [ underlayment ]
        , camera = camera
        , clipDepth = Length.meters 1
        , background = Scene3d.transparentBackground
        , dimensions = ( Pixels.int 800, Pixels.int 1000 )
        , upDirection = Direction3d.negativeZ
        }

Things that get rid of the Problem

Make scene not depend on the model

In the minimum example above, changing scene : Model -> Html msg to scene : Html msg gets rid of the problem. It means, of course, that the scene cannot change with the values in the model

Render it in Ellie

This whole thing came about because I tried to take a somewhat complicated Ellie I had worked on and work on it locally. That Ellie is a fork of the HelloWorld example from this package's examples/README.md document.

Here is an Ellie of the minimum example above that does not throw any errors. For that one I made sure to start from scratch on Ellie in case there was some weird state that fixes this problem in the HelloWorld example that carried through.

ap-nri commented 11 months ago

I'm outside of the edit window, but just to add: this isn't an issue of the nix packages version of elm being extremely weird; if I install elm with the official elm installer from elm-lang.org then call it directly with /usr/local/bin/elm make src/Main.elm --output=main.html it's the same issue.

ap-nri commented 11 months ago

Another user on the elm slack was not able to reproduce this problem from source but did see the same issue when I sent the compiled HTML file. So in the interests of debugging, I'm attaching it here. Github doesn't allow html attachments so here's a pastbin link https://pastebin.com/tUBCMgVM

ianmackenzie commented 11 months ago

There does seem to be some difference in Elm compiler versions somehow. I tried opening your HTML file locally and got the same errors; I compiled your source files with Elm 0.19.1 on Linux and it was fine. When I did a diff of the two HTML files against each other I see a bunch of stuff in the bad one that seems to be related to working around browser extensions; there's a new created_by_elm tag and some move complex logic that tries to skip over DOM nodes that Elm didn't itself create, e.g.:

image

Here's a Gist with the working HTML so you can do a diff yourself: https://gist.github.com/ianmackenzie/e8c9517106736e90bff772bdbab4bc9a

I'm not sure if there's much elm-3d-scene can really do here...my only guess is that elm-3d-scene uses a Html.Keyed.node "div" as the top-level HTML element, and maybe there's some quirk where some of the Elm virtual DOM logic doesn't work well if the very top-level element in <body> is a keyed node instead of a normal one? What happens if you try compiling with an additional dummy wrapper div around the Scene3d.cloudy call?

perkee commented 11 months ago

I've got the answer! It's not you, it's me! While I never applied jinjor's vdom patch, it was applied to my ~/.elm by work tooling!! And that patch breaks this!

ianmackenzie commented 11 months ago

Ah OK yeah it could be that patch doesn't work properly with keyed HTML nodes or something. Still kinda curious if a wrapper <div> would help though - that would be easy enough to add. (And as it happens I should have a new version of elm-3d-scene coming out sometime in the next month or so, so now's a pretty decent time to sneak something like that in.)

ianmackenzie commented 11 months ago

I guess to avoid accidentally breaking code that somehow relies on the existing elm-3d-scene internal HTML structure (e.g. via CSS selectors) maybe it's best to leave the internals as is...but if you do end up trying a wrapper <div> with the virtual DOM patch and that fixes things, let me know and I can add a note to the README or docs or something.

perkee commented 11 months ago

if you do end up trying a wrapper

with the virtual DOM patch and that fixes things, let me know and I can add a note to the README or docs or something.

Tragically wrapping it by div [] [ scene model ] does nothing to alleviate the problem. I have opened an issue in the patch repo https://github.com/jinjor/elm-break-dom/issues/35