├── .cargo └── config.toml ├── .github └── workflows │ ├── build-pages.yml │ ├── build.yml │ ├── integration-tests-build.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bindings ├── .gitignore ├── draco-decoder │ └── .gitkeep ├── noise │ ├── Cargo.toml │ ├── index.js │ └── src │ │ └── lib.rs ├── taffy │ ├── Cargo.toml │ ├── README.md │ ├── index.js │ ├── src │ │ ├── lib.rs │ │ └── utils.rs │ └── tests │ │ └── web.rs └── tools │ ├── build.cjs │ ├── generate-wasm-js.cjs │ ├── post-scripts.cjs │ └── remove-import-meta.cjs ├── fixtures ├── assets │ ├── 40.mp3 │ ├── 42.mp3 │ ├── 44.mp3 │ ├── 45.mp3 │ └── fan.mp3 ├── canvas │ ├── chatbox.xsml │ ├── particles.ts │ └── particles.xsml ├── css │ ├── css-transform.xsml │ ├── spatial-basic.xsml │ └── spatial-material-bumpmap.xsml ├── custom-loader │ ├── custom-load.xsml │ └── custom-resolve.xsml ├── dom │ ├── globals-in-script.xsml │ └── timers-in-script.xsml ├── esm │ ├── bar │ │ ├── ops.ts │ │ └── some.ts │ ├── dynamic-imports.xsml │ ├── exports-as-default.ts │ ├── exports-as-named.ts │ ├── files │ │ ├── babeee.bin │ │ ├── babeee.json │ │ └── babeee.wasm │ ├── foo.ts │ ├── http-imports.xsml │ ├── loaders.xsml │ ├── script-inline.xsml │ └── script-with-src.xsml ├── html-in-spatial │ ├── basic.xsml │ ├── images.xsml │ ├── paragraph.xsml │ ├── preact.xsml │ ├── text-button.ts │ └── text-buttons.xsml ├── material │ ├── raw-texture.ts │ └── raw-texture.xsml ├── model │ └── rokid-jungle.glb ├── package-lock.json ├── package.json ├── scripts-in-xml │ ├── exception-in-spaceready.xsml │ ├── external-async-scripts.xsml │ ├── external-blocking-scripts.xsml │ ├── first.ts │ ├── inline-scripts.xsml │ └── second.js ├── simple.xsml ├── spatial-element.xsml ├── spatial-externalmesh-glb.ts ├── spatial-externalmesh-glb.xsml ├── spatial-grid-system.ts ├── spatial-grid-system.xsml ├── spatial-lion.js ├── spatial-lion.xsml ├── spatial-morph-target.ts ├── spatial-morph-target.xsml ├── spatial-polyhedra-element.xsml ├── spatial-ui │ ├── layout-cylinder.xsml │ ├── layout-sphere.xsml │ └── layout-stack.xsml ├── spatial-vertices-updating.xsml ├── textures │ ├── bump.png │ ├── example.jpeg │ ├── splatting.jpg │ ├── stars.jpeg │ └── wall.jpeg ├── tsconfig.json ├── using-babylonjs │ ├── mesh-collision.js │ ├── mesh-collision.xsml │ ├── routing-car.js │ ├── routing-car.xsml │ ├── sun-particles.js │ └── sun-particles.xsml ├── websocket │ └── echo.xsml └── xr │ └── hand-tracking.xsml ├── jest.config.cjs ├── jest.setup.cjs ├── package-lock.json ├── package.json ├── pages ├── assets │ └── 3d_skill__role_badges_and_pins.glb ├── impl-babylonjs.ts ├── index.html ├── package-lock.json ├── package.json ├── screenshot.png ├── spatial-devtools │ └── console.xsml ├── tsconfig.json ├── webpack.config.cjs └── xr │ ├── Camera.ts │ ├── DefaultExperience.ts │ ├── ExperienceHelper.ts │ ├── FeatureManager.ts │ ├── Input.ts │ ├── InputSource.ts │ ├── LayerWrapper.ts │ ├── OutputCanvas.ts │ ├── README.md │ ├── RenderTargetTextureProvider.ts │ ├── SessionManager.ts │ ├── WebglLayer.ts │ └── features │ ├── WebXRAbstractFeature.ts │ ├── WebXRLayers.ts │ └── index.ts ├── prepublish.cjs ├── rustfmt.toml ├── spec └── README.md ├── src ├── README.md ├── agent │ ├── DeviceMemory.ts │ ├── cdp │ │ ├── api.ts │ │ ├── builder │ │ │ ├── .pdl_cache │ │ │ │ ├── .gitignore │ │ │ │ └── .gitkeep │ │ │ ├── main.ts │ │ │ ├── pdl-types.ts │ │ │ ├── pdl2json.ts │ │ │ └── pdl2typescript.ts │ │ ├── cancellation.ts │ │ ├── cdp-implementation.ts │ │ ├── cdp-protocol.ts │ │ ├── cdp-session.ts │ │ ├── client.ts │ │ ├── connection.ts │ │ ├── definitions.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── serializer │ │ │ ├── index.ts │ │ │ └── json.ts │ │ ├── server.ts │ │ └── transport │ │ │ ├── index.ts │ │ │ └── loopback.ts │ ├── console.ts │ ├── navigator.ts │ ├── parser │ │ ├── index.ts │ │ ├── xml-utils.ts │ │ └── xsml.ts │ ├── resources │ │ ├── .gitkeep │ │ ├── AssetsBundle.ts │ │ ├── ResourceQueue.test.ts │ │ └── ResourceQueue.ts │ ├── timers.ts │ └── window.ts ├── impl-headless.ts ├── impl-interfaces.ts ├── index.test.ts ├── index.ts ├── input-event.ts ├── live2 │ └── .gitkeep ├── live3 │ └── .gitkeep ├── living │ ├── attributes.ts │ ├── attributes │ │ ├── Attr.ts │ │ └── NamedNodeMap.ts │ ├── audiocontext │ │ └── Audio.ts │ ├── compatible.test.ts │ ├── crypto │ │ └── Noise.ts │ ├── cssom │ │ ├── CSSRule.ts │ │ ├── CSSRuleList.ts │ │ ├── CSSSpatialKeyframeRule.ts │ │ ├── CSSSpatialKeyframesRule.ts │ │ ├── CSSSpatialMaterialRule.ts │ │ ├── CSSSpatialStyleDeclaration.test.ts │ │ ├── CSSSpatialStyleDeclaration.ts │ │ ├── CSSSpatialStyleRule.ts │ │ ├── CSSStyleRule.ts │ │ ├── CSSStyleSheet.ts │ │ ├── README.md │ │ ├── StyleSheet.ts │ │ ├── StyleSheetList.ts │ │ ├── materials │ │ │ └── GridMaterial.ts │ │ ├── parsers │ │ │ ├── animation-shorthand │ │ │ │ ├── Error.ts │ │ │ │ ├── README.md │ │ │ │ ├── character.ts │ │ │ │ ├── fillAnimation.ts │ │ │ │ ├── getCubicBezier.test.ts │ │ │ │ ├── getCubicBezier.ts │ │ │ │ ├── getCustomIdent.test.ts │ │ │ │ ├── getCustomIdent.ts │ │ │ │ ├── getNumber.test.ts │ │ │ │ ├── getNumber.ts │ │ │ │ ├── getSteps.test.ts │ │ │ │ ├── getSteps.ts │ │ │ │ ├── getString.test.ts │ │ │ │ ├── getString.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── keyword.ts │ │ │ │ ├── parseAnimationShorthand.test.ts │ │ │ │ ├── parseAnimationShorthand.ts │ │ │ │ ├── parseSingleAnimationShorthand.test.ts │ │ │ │ ├── parseSingleAnimationShorthand.ts │ │ │ │ ├── serializeAnimation.test.ts │ │ │ │ ├── serializeAnimation.ts │ │ │ │ ├── serializeAnimationValue.test.ts │ │ │ │ ├── serializeAnimationValue.ts │ │ │ │ ├── serializeNumber.test.ts │ │ │ │ ├── serializeNumber.ts │ │ │ │ ├── shortest.test.ts │ │ │ │ ├── shortest.ts │ │ │ │ ├── skip.test.ts │ │ │ │ ├── skip.ts │ │ │ │ └── type.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── named-colors.ts │ │ ├── spatial-properties │ │ │ ├── albedo-color.ts │ │ │ ├── albedo-texture.ts │ │ │ ├── ambient-color.ts │ │ │ ├── ambient-texture.ts │ │ │ ├── animation-duration.ts │ │ │ ├── animation-iteration-count.ts │ │ │ ├── animation-name.ts │ │ │ ├── animation.ts │ │ │ ├── bump-texture.ts │ │ │ ├── diffuse-color.ts │ │ │ ├── diffuse-texture.ts │ │ │ ├── emissive-color.ts │ │ │ ├── emissive-texture.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ ├── material-alpha-mode.ts │ │ │ ├── material-grid-background-color.ts │ │ │ ├── material-grid-cell-height.ts │ │ │ ├── material-grid-cell-width.ts │ │ │ ├── material-grid-height.ts │ │ │ ├── material-grid-major-line-color.ts │ │ │ ├── material-grid-minor-line-color.ts │ │ │ ├── material-grid-width.ts │ │ │ ├── material-orientation.ts │ │ │ ├── material-type.ts │ │ │ ├── material.ts │ │ │ ├── physical-metallic.ts │ │ │ ├── physical-roughness.ts │ │ │ ├── position-x.ts │ │ │ ├── position-y.ts │ │ │ ├── position-z.ts │ │ │ ├── position.ts │ │ │ ├── rotation.ts │ │ │ ├── scaling.ts │ │ │ ├── specular-color.ts │ │ │ ├── specular-power.ts │ │ │ ├── specular-texture.ts │ │ │ └── wireframe.ts │ │ └── utils │ │ │ └── color-space.ts │ ├── custom-elements │ │ └── CustomElementRegistry.ts │ ├── domexception.test.ts │ ├── domexception.ts │ ├── domparsing │ │ ├── InnerHTML.ts │ │ └── serialization.ts │ ├── esm-supports.json │ ├── events │ │ ├── CloseEvent.ts │ │ ├── CustomEvent.ts │ │ ├── ErrorEvent.ts │ │ ├── FocusEvent.ts │ │ ├── HandTrackingEvent.ts │ │ ├── HashChangeEvent.ts │ │ ├── KeyboardEvent.ts │ │ ├── MessageEvent.ts │ │ ├── MouseEvent.ts │ │ ├── PopStateEvent.ts │ │ ├── ProgressEvent.ts │ │ ├── TouchEvent.ts │ │ └── UIEvent.ts │ ├── geometry │ │ ├── DOMMatrix.test.ts │ │ ├── DOMMatrix.ts │ │ ├── DOMMatrixReadOnly.ts │ │ ├── DOMPoint.test.ts │ │ ├── DOMPoint.ts │ │ ├── DOMPointReadOnly.test.ts │ │ ├── DOMPointReadOnly.ts │ │ ├── DOMRect.ts │ │ └── DOMRectReadOnly.ts │ ├── helpers │ │ ├── babylonjs │ │ │ ├── InteractiveDynamicTexture.ts │ │ │ ├── loaders │ │ │ │ ├── OBJ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── mtlFileLoader.ts │ │ │ │ │ ├── objFileLoader.ts │ │ │ │ │ ├── objLoadingOptions.ts │ │ │ │ │ └── solidParser.ts │ │ │ │ ├── README.md │ │ │ │ ├── STL │ │ │ │ │ ├── index.ts │ │ │ │ │ └── stlFileLoader.ts │ │ │ │ ├── gLTF │ │ │ │ │ ├── 1.0 │ │ │ │ │ │ ├── glTFBinaryExtension.ts │ │ │ │ │ │ ├── glTFLoader.ts │ │ │ │ │ │ ├── glTFLoaderInterfaces.ts │ │ │ │ │ │ ├── glTFLoaderUtils.ts │ │ │ │ │ │ ├── glTFMaterialsCommonExtension.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── 2.0 │ │ │ │ │ │ ├── Extensions │ │ │ │ │ │ │ ├── EXT_lights_image_based.ts │ │ │ │ │ │ │ ├── EXT_mesh_gpu_instancing.ts │ │ │ │ │ │ │ ├── EXT_meshopt_compression.ts │ │ │ │ │ │ │ ├── EXT_texture_webp.ts │ │ │ │ │ │ │ ├── ExtrasAsMetadata.ts │ │ │ │ │ │ │ ├── KHR_animation_pointer.data.ts │ │ │ │ │ │ │ ├── KHR_animation_pointer.ts │ │ │ │ │ │ │ ├── KHR_draco_mesh_compression.ts │ │ │ │ │ │ │ ├── KHR_lights_punctual.ts │ │ │ │ │ │ │ ├── KHR_materials_anisotropy.ts │ │ │ │ │ │ │ ├── KHR_materials_clearcoat.ts │ │ │ │ │ │ │ ├── KHR_materials_emissive_strength.ts │ │ │ │ │ │ │ ├── KHR_materials_ior.ts │ │ │ │ │ │ │ ├── KHR_materials_iridescence.ts │ │ │ │ │ │ │ ├── KHR_materials_pbrSpecularGlossiness.ts │ │ │ │ │ │ │ ├── KHR_materials_sheen.ts │ │ │ │ │ │ │ ├── KHR_materials_specular.ts │ │ │ │ │ │ │ ├── KHR_materials_translucency.ts │ │ │ │ │ │ │ ├── KHR_materials_transmission.ts │ │ │ │ │ │ │ ├── KHR_materials_unlit.ts │ │ │ │ │ │ │ ├── KHR_materials_variants.ts │ │ │ │ │ │ │ ├── KHR_materials_volume.ts │ │ │ │ │ │ │ ├── KHR_mesh_quantization.ts │ │ │ │ │ │ │ ├── KHR_texture_basisu.ts │ │ │ │ │ │ │ ├── KHR_texture_transform.ts │ │ │ │ │ │ │ ├── KHR_xmp_json_ld.ts │ │ │ │ │ │ │ ├── MSFT_audio_emitter.ts │ │ │ │ │ │ │ ├── MSFT_lod.ts │ │ │ │ │ │ │ ├── MSFT_minecraftMesh.ts │ │ │ │ │ │ │ ├── MSFT_sRGBFactors.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── glTFLoader.ts │ │ │ │ │ │ ├── glTFLoaderAnimation.ts │ │ │ │ │ │ ├── glTFLoaderExtension.ts │ │ │ │ │ │ ├── glTFLoaderInterfaces.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── glTFFileLoader.ts │ │ │ │ │ ├── glTFValidation.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── patches │ │ │ │ ├── draco_decoder_gltf.cjs │ │ │ │ ├── draco_decoder_gltf.wasm.cjs │ │ │ │ ├── index.ts │ │ │ │ ├── rewrite-draco-compression.ts │ │ │ │ └── rewrite-meshopt-compression.ts │ │ │ └── tags.ts │ │ ├── create-element.ts │ │ ├── custom-elements.ts │ │ ├── document-base-url.ts │ │ ├── gui2d │ │ │ ├── control.test.ts │ │ │ └── control.ts │ │ ├── internal-constants.ts │ │ ├── iterable-weak-set.ts │ │ ├── matrix-functions.test.ts │ │ ├── matrix-functions.ts │ │ ├── mutation-observers.ts │ │ ├── namespaces.ts │ │ ├── node.ts │ │ ├── ordered-set.ts │ │ ├── runtime-script-errors.ts │ │ ├── scripting-types.ts │ │ ├── selectors.ts │ │ ├── shadow-dom.ts │ │ ├── spatial-animations.ts │ │ ├── spatial-css-parser.ts │ │ ├── strings.ts │ │ ├── style-rules.ts │ │ ├── stylesheets.ts │ │ ├── text.ts │ │ ├── traversal.ts │ │ ├── url.test.ts │ │ ├── url.ts │ │ └── validate-names.ts │ ├── hr-time │ │ └── Performance.ts │ ├── image │ │ ├── ImageData.test.ts │ │ └── ImageData.ts │ ├── interfaces.test.ts │ ├── mutation-observer │ │ ├── MutationObserver.ts │ │ └── MutationRecord.ts │ ├── named-properties-tracker.ts │ ├── named-properties-window.ts │ ├── node-document-position.ts │ ├── node-type.ts │ ├── node.ts │ ├── nodes │ │ ├── CharacterData.ts │ │ ├── ChildNode.ts │ │ ├── DOMTokenList.ts │ │ ├── DocumentFragment.ts │ │ ├── DocumentOrShadowRoot.ts │ │ ├── DocumentType.ts │ │ ├── Element.ts │ │ ├── GlobalEventHandlers.ts │ │ ├── HTMLAudioElement.ts │ │ ├── HTMLBaseElement.ts │ │ ├── HTMLCollection.ts │ │ ├── HTMLContentElement.ts │ │ ├── HTMLDivElement.ts │ │ ├── HTMLElement.ts │ │ ├── HTMLHeadElement.ts │ │ ├── HTMLHeadingElement.ts │ │ ├── HTMLImageElement.ts │ │ ├── HTMLLinkElement.ts │ │ ├── HTMLMediaElement.ts │ │ ├── HTMLMetaElement.ts │ │ ├── HTMLParagraphElement.ts │ │ ├── HTMLScriptElement.test.ts │ │ ├── HTMLScriptElement.ts │ │ ├── HTMLSpanElement.ts │ │ ├── HTMLStyleElement.ts │ │ ├── HTMLTitleElement.ts │ │ ├── Node.ts │ │ ├── NodeList.test.ts │ │ ├── NodeList.ts │ │ ├── NonDocumentTypeChildNode.ts │ │ ├── NonElementParentNode.ts │ │ ├── ParentNode.ts │ │ ├── ShadowRoot.ts │ │ ├── SpatialBoundElement.ts │ │ ├── SpatialButtonElement.ts │ │ ├── SpatialCapsuleElement.ts │ │ ├── SpatialCubeElement.ts │ │ ├── SpatialCylinderElement.ts │ │ ├── SpatialDocument.ts │ │ ├── SpatialElement.ts │ │ ├── SpatialIcosphereElement.ts │ │ ├── SpatialMeshElement.ts │ │ ├── SpatialPanelElement.ts │ │ ├── SpatialPlaneElement.ts │ │ ├── SpatialPolyhedraElement.ts │ │ ├── SpatialRefElement.ts │ │ ├── SpatialSpaceElement.ts │ │ ├── SpatialSphereElement.ts │ │ ├── SpatialTorusElement.ts │ │ └── Text.ts │ ├── range │ │ ├── AbstractRange.ts │ │ └── Range.ts │ ├── script-context.ts │ ├── traversal │ │ ├── NodeIterator.ts │ │ └── helpers.ts │ └── xr │ │ ├── XRFrame.ts │ │ ├── XRHand.ts │ │ ├── XRInputSource.ts │ │ ├── XRInputSourceArray.ts │ │ ├── XRJointSpace.ts │ │ ├── XRPose.ts │ │ ├── XRRigidTransform.ts │ │ ├── XRSession.ts │ │ ├── XRSpace.ts │ │ └── XRSystem.ts ├── mixin.test.ts ├── mixin.ts ├── symbols.ts ├── utils.test.ts ├── utils.ts └── virtual-console.ts ├── tools └── generate-dom-interfaces.js ├── tsconfig-esm-loader.js ├── tsconfig.json ├── types ├── .gitignore ├── .npmignore ├── .package.json ├── build.cjs ├── index.d.ts ├── package-lock.json └── tsconfig.json ├── version.cjs └── webpack.config.cjs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [profile.dev] 2 | opt-level = 0 3 | 4 | [profile.release] 5 | opt-level = "s" 6 | strip = true 7 | -------------------------------------------------------------------------------- /.github/workflows/build-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Pages 2 | 3 | on: 4 | schedule: 5 | - cron: '0 10 * * *' 6 | push: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build: 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest] 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Set up Rust 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: 1.74.0 36 | - uses: actions/setup-node@v4.0.0 37 | with: 38 | node-version: 18.16.0 39 | - name: Install Node.js dependencies 40 | run: npm ci && npx tsc --version 41 | - name: Setup Pages 42 | uses: actions/configure-pages@v3 43 | - name: Install pages dependencies 44 | working-directory: ./pages 45 | run: npm ci 46 | - name: Generate interfaces.ts 47 | run: | 48 | npm run gen-dom-interfaces 49 | - name: Build bindings 50 | run: | 51 | npm run bindings 52 | - name: Build Pages 53 | working-directory: ./pages 54 | run: npx webpack 55 | - name: Upload artifact 56 | uses: actions/upload-pages-artifact@v2 57 | with: 58 | path: './pages' 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v2 62 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | schedule: 5 | - cron: '0 10 * * *' 6 | push: 7 | branches: [ "main" ] 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: 1.74.0 25 | - uses: actions/setup-node@v4.0.0 26 | with: 27 | node-version: 18.16.0 28 | - name: Install Node.js dependencies 29 | run: npm ci && npx tsc --version 30 | - name: Generate interfaces.ts 31 | run: | 32 | npm run gen-dom-interfaces 33 | - name: Build bindings 34 | run: | 35 | npm run bindings 36 | - name: Run Tests 37 | run: npm test 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | types/ 3 | bindings/ 4 | fixtures/ 5 | node_modules/ 6 | pages/ 7 | coverage/ 8 | .github/ 9 | 10 | # rust 11 | target/ 12 | *.toml 13 | 14 | !dist/ 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "bindings/noise", 5 | "bindings/taffy", 6 | ] 7 | -------------------------------------------------------------------------------- /bindings/.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /bindings/draco-decoder/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/bindings/draco-decoder/.gitkeep -------------------------------------------------------------------------------- /bindings/noise/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noise-binding" 3 | version = "0.1.0" 4 | authors = ["Yorkie Makoto "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = "0.2.84" 15 | js-sys = "0.3" 16 | noise = "0.8.2" 17 | console_error_panic_hook = { version = "0.1.7", optional = true } 18 | 19 | [dev-dependencies] 20 | wasm-bindgen-test = "0.3.34" 21 | 22 | [profile.release] 23 | opt-level = "s" 24 | -------------------------------------------------------------------------------- /bindings/noise/index.js: -------------------------------------------------------------------------------- 1 | import * as noise from './pkg/noise_binding'; 2 | 3 | let initNoiseAsync = noise.default; 4 | if (typeof initNoiseAsync !== 'function') { 5 | initNoiseAsync = initNoiseAsync.default; 6 | } 7 | 8 | let loaded = false; 9 | export async function loadNoise() { 10 | if (loaded) { 11 | return; 12 | } else { 13 | const wasmBinary = await import('./pkg/noise_binding_bg_wasm'); 14 | await initNoiseAsync(wasmBinary.default); 15 | loaded = true; 16 | } 17 | } 18 | 19 | export * from './pkg/noise_binding'; 20 | -------------------------------------------------------------------------------- /bindings/taffy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "taffy-binding" 3 | version = "0.1.0" 4 | authors = ["Yorkie Makoto "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = "0.2.84" 15 | js-sys = "0.3" 16 | taffy = "0.3.18" 17 | console_error_panic_hook = { version = "0.1.7", optional = true } 18 | 19 | [dev-dependencies] 20 | wasm-bindgen-test = "0.3.34" 21 | 22 | [profile.release] 23 | opt-level = "s" 24 | -------------------------------------------------------------------------------- /bindings/taffy/README.md: -------------------------------------------------------------------------------- 1 | # Taffy Binding 2 | 3 | This is a fork of this project: https://github.com/load1n9/gelatin, which is a Deno binding library for the high-performance layout library [taffy][]. 4 | 5 | [taffy]: https://docs.rs/taffy/latest/taffy/ 6 | 7 | ## Build 8 | 9 | Using [wasm-pack][] to build: 10 | 11 | ``` 12 | $ wasm-pack --verbose build --target web 13 | ``` 14 | -------------------------------------------------------------------------------- /bindings/taffy/index.js: -------------------------------------------------------------------------------- 1 | import * as taffy from './pkg/taffy_binding'; 2 | 3 | let initTaffyAsync = taffy.default; 4 | if (typeof initTaffyAsync !== 'function') { 5 | /** 6 | * FIXME: this is a workaround for the fact that the ESM build by ts-jest. 7 | * 8 | * When a esm module has default export and named exports, "ts-jest/presets.jsWithTsESM" will 9 | * generate a module like this: 10 | * 11 | * ```js 12 | * [Module: null prototype] { 13 | * a; 14 | * b; 15 | * default: { 16 | * a; 17 | * b; 18 | * default: ... <= Exactly the real default export 19 | * } 20 | * } 21 | * ``` 22 | * 23 | * It seems that the default export is wrapped in another object, but other build tools like 24 | * webpack generates a module like this: 25 | * 26 | * ```js 27 | * { 28 | * a; 29 | * b; 30 | * default: ... <= Exactly the real default export 31 | * } 32 | * ``` 33 | * 34 | * This workaround is to check if the first result of `taffy.default` is a function, if not, 35 | * we assume that the default export is wrapped in another object. 36 | * 37 | * Does anyone know why this happens and how to fix it? 38 | */ 39 | initTaffyAsync = initTaffyAsync.default; 40 | } 41 | 42 | let loaded = false; 43 | export async function loadTaffy() { 44 | if (loaded) { 45 | return; 46 | } else { 47 | const wasmBinary = await import('./pkg/taffy_binding_bg_wasm'); 48 | await initTaffyAsync(wasmBinary.default); 49 | loaded = true; 50 | } 51 | } 52 | 53 | export * from './pkg/taffy_binding'; 54 | -------------------------------------------------------------------------------- /bindings/taffy/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /bindings/taffy/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /bindings/tools/build.cjs: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | [ 4 | '../taffy', 5 | '../noise', 6 | ].forEach(path => { 7 | execSync(`npx wasm-pack build ${path} --target web`, { 8 | stdio: 'inherit', 9 | cwd: __dirname 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /bindings/tools/generate-wasm-js.cjs: -------------------------------------------------------------------------------- 1 | const { glob } = require('glob'); 2 | const fsPromises = require('fs/promises'); 3 | 4 | glob('./**/pkg/*.wasm').then((files) => { 5 | files.forEach(async (file) => { 6 | const data = await fsPromises.readFile(file); 7 | const arrayStr = data.toJSON().data.toString(); 8 | 9 | const targetPath = file.replace(/\.wasm$/, '_wasm.ts'); 10 | await fsPromises.writeFile(targetPath, ` 11 | export default new Uint8Array([${arrayStr}]); 12 | `.replace(/^\n/, '')); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /bindings/tools/post-scripts.cjs: -------------------------------------------------------------------------------- 1 | require('./generate-wasm-js.cjs'); 2 | require('./remove-import-meta.cjs'); 3 | -------------------------------------------------------------------------------- /bindings/tools/remove-import-meta.cjs: -------------------------------------------------------------------------------- 1 | const { glob } = require('glob'); 2 | const fsPromises = require('fs/promises'); 3 | 4 | glob('./**/pkg/*.js').then((files) => { 5 | files.forEach(async (file) => { 6 | const jsSourceCode = await fsPromises.readFile(file, 'utf8'); 7 | const fixedSourceCode = jsSourceCode 8 | .replace(/import\.meta\.url/g, `''/** import.meta.url */`); 9 | fsPromises.writeFile(file, fixedSourceCode, 'utf8'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /fixtures/assets/40.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/assets/40.mp3 -------------------------------------------------------------------------------- /fixtures/assets/42.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/assets/42.mp3 -------------------------------------------------------------------------------- /fixtures/assets/44.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/assets/44.mp3 -------------------------------------------------------------------------------- /fixtures/assets/45.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/assets/45.mp3 -------------------------------------------------------------------------------- /fixtures/assets/fan.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/assets/fan.mp3 -------------------------------------------------------------------------------- /fixtures/canvas/chatbox.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial CSS Example (Bump Map) 4 | 5 | 6 | 7 | 8 | 35 | 36 | -------------------------------------------------------------------------------- /fixtures/canvas/particles.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canvas API Example (Particles) 4 | 17 | 18 | 19 | 20 | 21 | Particles Example 22 | 28 | 29 | 30 | 31 | 55 | 56 | -------------------------------------------------------------------------------- /fixtures/css/spatial-basic.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial CSS Example (Basic) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | -------------------------------------------------------------------------------- /fixtures/css/spatial-material-bumpmap.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial CSS Example (Bump Map) 4 | 5 | 6 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /fixtures/custom-loader/custom-load.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Framework Script Example (Custom Loader for resolve) 4 | 18 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /fixtures/custom-loader/custom-resolve.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Framework Script Example (Custom Loader for resolve) 4 | 16 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /fixtures/dom/globals-in-script.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example 4 | 5 | 6 | 7 | 22 | 23 | -------------------------------------------------------------------------------- /fixtures/dom/timers-in-script.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /fixtures/esm/bar/ops.ts: -------------------------------------------------------------------------------- 1 | import { some } from './some'; 2 | 3 | export function sum(a: number, b: number) { 4 | return a + b; 5 | } 6 | 7 | export function sumWithSome(a: number) { 8 | return sum(a, some); 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/esm/bar/some.ts: -------------------------------------------------------------------------------- 1 | export const some = 14; 2 | -------------------------------------------------------------------------------- /fixtures/esm/dynamic-imports.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Loader Example 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /fixtures/esm/exports-as-default.ts: -------------------------------------------------------------------------------- 1 | export default function hello(input: string) { 2 | console.log('hello default exports', `@${input}`); 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/esm/exports-as-named.ts: -------------------------------------------------------------------------------- 1 | export function hello(input: string) { 2 | console.log('hello named exports', input); 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/esm/files/babeee.bin: -------------------------------------------------------------------------------- 1 | babeee 2 | -------------------------------------------------------------------------------- /fixtures/esm/files/babeee.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babeee" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/esm/files/babeee.wasm: -------------------------------------------------------------------------------- 1 | babeee 2 | -------------------------------------------------------------------------------- /fixtures/esm/foo.ts: -------------------------------------------------------------------------------- 1 | import helloDefault from './exports-as-default'; 2 | import { hello } from './exports-as-named'; 3 | 4 | console.log('foobar in esm/foo.ts'); 5 | helloDefault('foo'); 6 | hello('foo'); 7 | 8 | import { some } from './bar/some'; 9 | import { sum, sumWithSome } from './bar/ops'; 10 | console.log(some); 11 | console.log(sum(some, 10) === sumWithSome(10)); 12 | console.log(sum); 13 | -------------------------------------------------------------------------------- /fixtures/esm/http-imports.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Imports from HTTP/HTTPS 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fixtures/esm/loaders.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Loader Example 4 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /fixtures/esm/script-inline.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Script Example (Inline) 4 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /fixtures/esm/script-with-src.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Script Example (External) 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fixtures/html-in-spatial/images.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HTML in Spatial Example (Images) 4 | 5 | 6 | 7 | 24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /fixtures/html-in-spatial/paragraph.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HTML in Spatial Example (Images) 4 | 15 | 16 | 17 | 18 | 19 | 35 |
36 |

Using a section without a heading

37 |

Heading 2

38 |

Heading 3

39 |

Heading 4

40 |
Heading 5
41 |

42 | Depending on the content, including a heading could also be good for SEO, so it is an option to consider. 43 |

44 | 45 |

46 | Circumstances where you might see used without a heading are typically found in web application/UI sections rather than in traditional document structures. 47 | In a document, it doesn't really make any sense to have a separate section of content without a heading to describe its contents. Such headings are useful 48 |

49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /fixtures/html-in-spatial/preact.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HTML in Spatial Example (Preact) 4 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 29 |
30 | -------------------------------------------------------------------------------- /fixtures/html-in-spatial/text-buttons.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GUI Buttons 4 | 10 | 11 | 12 | 13 | 14 | 15 | 38 |
39 | 40 | Do 41 | Ri 42 | Mi 43 | Fa 44 | 45 | 46 | 空间小程序还原最开始的 Web 开发体验,并所见即所得。 47 | 48 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /fixtures/material/raw-texture.ts: -------------------------------------------------------------------------------- 1 | import wallPic from '../textures/wall.jpeg'; 2 | 3 | const scene = spatialDocument.scene; 4 | const wall = spatialDocument.getSpatialObjectById('box1'); 5 | 6 | createImageBitmap(new Blob([wallPic], { type: 'image/jpeg' })).then((bitmap) => { 7 | const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); 8 | const ctx = canvas.getContext('2d'); 9 | ctx.drawImage(bitmap, 0, 0); 10 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 11 | const wallTexture = new BABYLON.RawTexture( 12 | imageData.data, 13 | imageData.width, 14 | imageData.height, 15 | BABYLON.Engine.TEXTUREFORMAT_RGBA, 16 | scene, 17 | false, 18 | false, 19 | BABYLON.Texture.TRILINEAR_SAMPLINGMODE); 20 | 21 | const wallMaterial = wall.asNativeType().material as BABYLON.StandardMaterial; 22 | wallMaterial.diffuseTexture = wallTexture; 23 | }); 24 | -------------------------------------------------------------------------------- /fixtures/material/raw-texture.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Material Example (Basic) 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /fixtures/model/rokid-jungle.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/model/rokid-jungle.glb -------------------------------------------------------------------------------- /fixtures/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "fixtures", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@yodaos-jsar/types": "file:../types" 13 | } 14 | }, 15 | "../types": { 16 | "version": "0.3.0-alpha.20231214.1702538392039", 17 | "license": "Apache-2.0", 18 | "dependencies": { 19 | "@types/node": "^18.12.1", 20 | "babylonjs": "^6.10.0" 21 | } 22 | }, 23 | "node_modules/@yodaos-jsar/types": { 24 | "resolved": "../types", 25 | "link": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "html-in-spatial/text-buttons.xsml", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@yodaos-jsar/types": "file:../types" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/exception-in-spaceready.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scripts in XML (Exception in spaceready event) 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/external-async-scripts.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scripts in XML (External async scripts) 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/external-blocking-scripts.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scripts in XML (External blocking scripts) 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/first.ts: -------------------------------------------------------------------------------- 1 | document.first = 'first'; 2 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/inline-scripts.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scripts in XML (Inline) 4 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fixtures/scripts-in-xml/second.js: -------------------------------------------------------------------------------- 1 | document.second = 'second'; 2 | -------------------------------------------------------------------------------- /fixtures/simple.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/spatial-externalmesh-glb.ts: -------------------------------------------------------------------------------- 1 | spatialDocument.addEventListener('spaceReady', () => { 2 | const scene = spatialDocument.scene; 3 | const animations = scene.animationGroups 4 | .filter(ag => ag.name.startsWith('model.')); 5 | 6 | if (animations.length > 0) { 7 | animations[0].start(true); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /fixtures/spatial-externalmesh-glb.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | External Mesh Example(Glb) 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fixtures/spatial-grid-system.ts: -------------------------------------------------------------------------------- 1 | 2 | const container = spatialDocument.querySelector('#box-bound'); 3 | const gamePanel = spatialDocument.querySelector('#game-panel'); 4 | 5 | let movingBlock: BABYLON.Mesh = null; 6 | function createNewBlock() { 7 | const block = spatialDocument.createElement('cube'); 8 | block.size = 0.1; 9 | container.appendChild(block); 10 | 11 | block.position = new DOMPoint(0, 0.45, 0); 12 | return block; 13 | } 14 | 15 | function fallDown() { 16 | if (movingBlock == null) { 17 | return; 18 | } 19 | movingBlock.position.y -= 0.1; 20 | if (movingBlock.position.y < -0.45) { 21 | movingBlock = null; 22 | } 23 | } 24 | 25 | const startBtn = gamePanel.shadowRoot.querySelector('#start-btn'); 26 | if (startBtn) { 27 | startBtn.addEventListener('mouseup', () => { 28 | movingBlock = createNewBlock().asNativeType(); 29 | }); 30 | } 31 | 32 | setInterval(() => { 33 | fallDown(); 34 | }, 1000); 35 | -------------------------------------------------------------------------------- /fixtures/spatial-grid-system.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Morph Target Example 4 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 56 |
57 |
Start
58 |
59 |
60 | 61 |
62 |
63 | -------------------------------------------------------------------------------- /fixtures/spatial-morph-target.ts: -------------------------------------------------------------------------------- 1 | const scene = spatialDocument.scene; 2 | const sphereElement = spatialDocument.querySelector('sphere'); 3 | 4 | const sphere = sphereElement.asNativeType(); 5 | const sphere2 = BABYLON.Mesh.CreateSphere('sphere2', 16, 2, scene); 6 | sphere2.setEnabled(false); 7 | sphere2.updateMeshPositions(function scrambleUp(data) { 8 | for (let index = 0; index < data.length; index++) { 9 | data[index] += 0.4 * Math.random(); 10 | } 11 | }); 12 | 13 | const manager = new BABYLON.MorphTargetManager(); 14 | sphere.morphTargetManager = manager; 15 | 16 | const target0 = BABYLON.MorphTarget.FromMesh(sphere2, 'sphere2', 0.25); 17 | manager.addTarget(target0); 18 | let angle = 0; 19 | 20 | scene.registerBeforeRender(function () { 21 | target0.influence = Math.sin(angle) * Math.sin(angle); 22 | angle += 0.01; 23 | }); 24 | -------------------------------------------------------------------------------- /fixtures/spatial-morph-target.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Morph Target Example 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fixtures/spatial-polyhedra-element.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Polyhedra Example 4 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /fixtures/spatial-ui/layout-cylinder.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial User Interface Example (Stack Layout) 4 | 5 | 6 | 7 | Button 1 8 | Button 2 9 | Button 3 10 | Button 4 11 | Button 5 12 | Button 6 13 | Button 7 14 | Button 8 15 | Button 9 16 | Button 10 17 | Button 11 18 | Button 12 19 | Button 13 20 | Button 14 21 | Button 15 22 | Button 16 23 | Button 17 24 | Button 18 25 | Button 19 26 | Button 20 27 | 28 | 29 | 37 | 38 | -------------------------------------------------------------------------------- /fixtures/spatial-ui/layout-sphere.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial User Interface Example (Stack Layout) 4 | 5 | 6 | 7 | Button 1 8 | Button 2 9 | Button 3 10 | Button 4 11 | Button 5 12 | Button 6 13 | Button 7 14 | Button 8 15 | Button 9 16 | Button 10 17 | Button 11 18 | Button 12 19 | Button 13 20 | Button 14 21 | Button 15 22 | Button 16 23 | Button 17 24 | Button 18 25 | Button 19 26 | Button 20 27 | 28 | 29 | 43 | 44 | -------------------------------------------------------------------------------- /fixtures/spatial-ui/layout-stack.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spatial User Interface Example (Stack Layout) 4 | 5 | 6 | 7 | 8 | Change Color 9 | Orientation 10 | Change Color 11 | 12 | 13 | 14 | 42 | 60 | 61 | -------------------------------------------------------------------------------- /fixtures/textures/bump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/textures/bump.png -------------------------------------------------------------------------------- /fixtures/textures/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/textures/example.jpeg -------------------------------------------------------------------------------- /fixtures/textures/splatting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/textures/splatting.jpg -------------------------------------------------------------------------------- /fixtures/textures/stars.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/textures/stars.jpeg -------------------------------------------------------------------------------- /fixtures/textures/wall.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/fixtures/textures/wall.jpeg -------------------------------------------------------------------------------- /fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "types": [ 6 | "node", 7 | "@yodaos-jsar/types" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /fixtures/using-babylonjs/mesh-collision.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Using with Babylon.js (Mesh Collision) 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fixtures/using-babylonjs/routing-car.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Using with Babylon.js (Routing Car) 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fixtures/using-babylonjs/sun-particles.js: -------------------------------------------------------------------------------- 1 | const scene = spatialDocument.scene; 2 | BABYLON.ParticleHelper.CreateAsync('sun', scene).then((set) => { 3 | set.start(); 4 | }); 5 | -------------------------------------------------------------------------------- /fixtures/using-babylonjs/sun-particles.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Using with Babylon.js (Sun Particles) 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fixtures/websocket/echo.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebSocket Example (Echo) 4 | 5 | 6 | 7 | 19 |
20 |

Echo Response Testing

21 |

Connect to wss://echo.websocket.org

22 |

Response: waiting...

23 |
24 |
25 |
26 | 44 |
45 | -------------------------------------------------------------------------------- /fixtures/xr/hand-tracking.xsml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebXR Example (Hand Tracking) 4 | 5 | 10 | 11 | 12 | 13 | 14 | 27 | 28 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | const presets = require('ts-jest/presets'); 2 | 3 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 4 | module.exports = { 5 | transform: { 6 | ...presets.jsWithTsESM.transform, 7 | }, 8 | testEnvironment: 'node', 9 | extensionsToTreatAsEsm: ['.ts'], 10 | collectCoverageFrom: [ 11 | 'src/**/*.ts', 12 | '!node_modules/**', 13 | ], 14 | setupFiles: [ 15 | './jest.setup.cjs', 16 | ], 17 | coverageReporters: [ 18 | 'html', 19 | 'text-summary', 20 | 'cobertura', 21 | ], 22 | moduleNameMapper: { 23 | '@bindings/craft3d': '/bindings/craft3d', 24 | '@bindings/taffy': '/bindings/taffy', 25 | '@bindings/noise': '/bindings/noise', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /jest.setup.cjs: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | // Since many modules require babylonjs and jest does not have it, which must be imported globally. 3 | global.BABYLON = require('babylonjs'); 4 | 5 | // Patch requestAnimationFrame to run all animations synchronously 6 | global.requestAnimationFrame = function (callback) { 7 | setTimeout(callback, 0); 8 | }; 9 | -------------------------------------------------------------------------------- /pages/assets/3d_skill__role_badges_and_pins.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/pages/assets/3d_skill__role_badges_and_pins.glb -------------------------------------------------------------------------------- /pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsar-dom-pages", 3 | "version": "1.0.0", 4 | "description": "The pages for JSAR-DOM", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=dev webpack serve" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/wicg-file-system-access": "^2023.10.4", 14 | "assert-browserify": "^2.0.0", 15 | "buffer": "^5.7.1", 16 | "compression-webpack-plugin": "^10.0.0", 17 | "path-browserify": "^1.0.1", 18 | "process": "^0.11.10", 19 | "terser-webpack-plugin": "^5.3.9", 20 | "ts-loader": "^9.5.1", 21 | "url": "^0.11.3", 22 | "util": "^0.12.5", 23 | "webpack": "^5.89.0", 24 | "webpack-cli": "^5.1.4", 25 | "webpack-dev-server": "^4.15.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/pages/screenshot.png -------------------------------------------------------------------------------- /pages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /pages/xr/README.md: -------------------------------------------------------------------------------- 1 | XR Integration 2 | =============== 3 | 4 | The XR integration is for opening the JSAR-DOM application in Mozila's WebXR Viewer and other WebXR compatible browsers. 5 | 6 | ### Why not using the BABYLON's WebXR? 7 | 8 | The BABYLON's WebXR is not working with Mozila's WebXR Viewer, in this directory we have copied the initial code from BABYLON's WebXR and modified it to work with Mozila's WebXR Viewer. 9 | -------------------------------------------------------------------------------- /pages/xr/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WebXRAbstractFeature'; 2 | export * from './WebXRLayers'; 3 | -------------------------------------------------------------------------------- /prepublish.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises'); 2 | const packageJson = require('./package.json'); 3 | 4 | packageJson.dependencies = {}; 5 | fs.writeFile('./package.json', JSON.stringify(packageJson, null, 2)); 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # rustfmt.toml 2 | 3 | tab_spaces = 2 4 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # JSAR Specification 2 | 3 | In this directory you will find the specification for the JSAR-related formats. The specification is written in [BikeShed][], and can be compiled to HTML using the `make` command. 4 | 5 | ## Requirements 6 | 7 | - [pipx][] 8 | - [BikeShed][] 9 | 10 | ## Building 11 | 12 | To build the specification, run `make` in this directory. This will install the required dependencies and build the specification. 13 | 14 | ```sh 15 | $ make spec 16 | ``` 17 | 18 | [BikeShed]: https://speced.github.io/bikeshed/ 19 | [pipx]: https://github.com/pypa/pipx 20 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # DOM 2 | 3 | This is a DOM implementation for JSAR runtime, it is based on [jsdom](https://github.com/jsdom/jsdom) partially and use TypeScript to implement the living standard. 4 | 5 | Notable changes from jsdom: 6 | 7 | - Add a base class `XSMLBaseDocument` which extends from `Node`, and it's used for `SpatialDocument` and `XSMLShadowRoot`. 8 | - `SpatialDocument` is used to represent the spatial document context. 9 | - `XSMLShadowRoot` is used to represent an interactive DOM tree which could be attached to a spatial object as a dynamic texture. 10 | - Change the `Node`, `Element`'s `ownerDocument` to be `XSMLBaseDocument` instead of `Document`. 11 | 12 | > Note: This is not a Web polyfill for JSAR runtime, which will be available for JSAR user-land, for example, the `EventTarget` polyfill doesn't implemented here which is depended by JSAR runtime itself, too. 13 | -------------------------------------------------------------------------------- /src/agent/DeviceMemory.ts: -------------------------------------------------------------------------------- 1 | import { NavigatorImpl } from './navigator'; 2 | 3 | export default interface DeviceMemoryImpl extends NavigatorImpl { }; 4 | export default class DeviceMemoryImpl { 5 | get deviceMemory(): number { 6 | return this._nativeUserAgent.deviceMemory; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/agent/cdp/builder/.pdl_cache/.gitignore: -------------------------------------------------------------------------------- 1 | *.pdl 2 | *.json 3 | -------------------------------------------------------------------------------- /src/agent/cdp/builder/.pdl_cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/src/agent/cdp/builder/.pdl_cache/.gitkeep -------------------------------------------------------------------------------- /src/agent/cdp/builder/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { promises as fs } from 'fs'; 6 | import * as path from 'path'; 7 | import { pdlUrlToJson } from './pdl2json'; 8 | import { pdlToTypeScript } from './pdl2typescript'; 9 | import { platform } from 'os'; 10 | 11 | const sources = new Map([ 12 | [ 13 | 'CdpV8', 14 | 'https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/js_protocol.json', 15 | ], 16 | [ 17 | 'CdpBrowser', 18 | 'https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/browser_protocol.json', 19 | ], 20 | ]); 21 | 22 | let target = path.join(new URL(import.meta.url).pathname, '../../definitions.ts'); 23 | if (platform() === 'win32' && target[0] === '\\') { 24 | target = target.slice(1); 25 | } 26 | 27 | async function main() { 28 | const definitions = pdlToTypeScript( 29 | await Promise.all( 30 | [...sources].map(async ([name, url]) => ({ 31 | name, 32 | definition: await pdlUrlToJson(url), 33 | })), 34 | ), 35 | ); 36 | await fs.writeFile(target, definitions); 37 | } 38 | 39 | main().catch(err => { 40 | console.error(err); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /src/agent/cdp/builder/pdl2json.ts: -------------------------------------------------------------------------------- 1 | import { basename, join } from 'path'; 2 | import fs from 'node:fs'; 3 | import { platform } from 'os'; 4 | 5 | export async function pdlUrlToJson(pdlUrl: string) { 6 | let content: string; 7 | const urlObj = new URL(pdlUrl); 8 | const filename = basename(urlObj.pathname); 9 | let cacheFilename = join(new URL(import.meta.url).pathname, '../.pdl_cache', filename); 10 | if (platform() === 'win32' && cacheFilename[0] === '\\') { 11 | cacheFilename = cacheFilename.slice(1); 12 | } 13 | console.info(`request pdl file from ${pdlUrl}`); 14 | 15 | if (fs.existsSync(cacheFilename)) { 16 | console.info('use cached protocol:', cacheFilename); 17 | content = await fs.promises.readFile(cacheFilename, 'utf8'); 18 | } else { 19 | content = await (await fetch(pdlUrl)).text(); 20 | } 21 | return JSON.parse(content); 22 | } 23 | -------------------------------------------------------------------------------- /src/agent/cdp/cancellation.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { Event, IDisposable, TaskCancelledError } from 'cockatiel'; 6 | 7 | export interface ICancellationToken { 8 | readonly onCancellationRequested: Event; 9 | readonly isCancellationRequested: boolean; 10 | } 11 | 12 | /** 13 | * Returns the result of the promise if it resolves before the cancellation 14 | * is requested. Otherwise, throws a TaskCancelledError. 15 | */ 16 | export function timeoutPromise( 17 | promise: Promise, 18 | cancellation: ICancellationToken, 19 | message?: string, 20 | ): Promise { 21 | if (cancellation.isCancellationRequested) { 22 | return Promise.reject(new TaskCancelledError(message || 'Task cancelled')); 23 | } 24 | 25 | let disposable: IDisposable; 26 | 27 | return Promise.race([ 28 | new Promise((_resolve, reject) => { 29 | disposable = cancellation.onCancellationRequested(() => 30 | reject(new TaskCancelledError(message || 'Task cancelled')), 31 | ); 32 | }), 33 | promise.finally(() => disposable.dispose()), 34 | ]); 35 | } 36 | -------------------------------------------------------------------------------- /src/agent/cdp/cdp-protocol.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-namespace 6 | export namespace CdpProtocol { 7 | export interface ICommand { 8 | id?: number; 9 | method: string; 10 | params: unknown; 11 | sessionId?: string; 12 | } 13 | 14 | export interface IError { 15 | id: number; 16 | error: { code: number; message: string }; 17 | sessionId?: string; 18 | } 19 | 20 | export interface ISuccess { 21 | id: number; 22 | result: unknown; 23 | sessionId?: string; 24 | } 25 | 26 | export type Message = ICommand | ISuccess | IError; 27 | 28 | export const isCommand = (message: Message): message is ICommand => 'method' in message; 29 | export const isResponse = (message: Message): message is IError | ISuccess => 30 | 'error' in message || 'result' in message; 31 | } 32 | -------------------------------------------------------------------------------- /src/agent/cdp/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | export * from './api'; 6 | export { ICancellationToken } from './cancellation'; 7 | export * from './cdp-protocol'; 8 | export * from './client'; 9 | export * from './connection'; 10 | export * from './definitions'; 11 | export * from './errors'; 12 | export * from './serializer/index'; 13 | export * from './server'; 14 | export * from './transport/index'; 15 | -------------------------------------------------------------------------------- /src/agent/cdp/serializer/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { CdpProtocol } from '../cdp-protocol'; 6 | import { Transportable } from '../transport'; 7 | 8 | export interface ISerializer { 9 | /** 10 | * Serializes the message for the wire. 11 | */ 12 | serialize(message: CdpProtocol.Message): Transportable; 13 | 14 | /** 15 | * Deserializes a message from the wire. 16 | */ 17 | deserialize(message: Transportable): CdpProtocol.Message; 18 | } 19 | -------------------------------------------------------------------------------- /src/agent/cdp/serializer/json.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { TextDecoder } from 'util'; 6 | import { CdpProtocol } from '../cdp-protocol'; 7 | import { Transportable } from '../transport'; 8 | import { ISerializer } from './index'; 9 | 10 | export class JsonSerializer implements ISerializer { 11 | private decoder?: TextDecoder; 12 | 13 | serialize(message: CdpProtocol.Message): Transportable { 14 | return JSON.stringify(message); 15 | } 16 | 17 | deserialize(message: Transportable): CdpProtocol.Message { 18 | if (typeof message !== 'string') { 19 | this.decoder ??= new TextDecoder(); 20 | message = this.decoder.decode(message); 21 | } 22 | 23 | return JSON.parse(message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/agent/cdp/transport/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { Event, IDisposable } from 'cockatiel'; 6 | 7 | export type Transportable = string | Uint8Array; 8 | 9 | export interface ITransport extends IDisposable { 10 | /** 11 | * Event that fires when a message is received. 12 | */ 13 | readonly onMessage: Event; 14 | 15 | /** 16 | * Event that fires when the transport is closed, possibly with an error 17 | * that caused the transport to close. 18 | */ 19 | readonly onEnd: Event; 20 | 21 | /** 22 | * Sends a serialized message over the transport. 23 | */ 24 | send(message: Transportable): void; 25 | } 26 | 27 | export { LoopbackTransport } from './loopback'; 28 | -------------------------------------------------------------------------------- /src/agent/cdp/transport/loopback.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { EventEmitter } from 'cockatiel'; 6 | import { ITransport, Transportable } from './index'; 7 | 8 | /** 9 | * Transport that allows manual control, useful for testing. 10 | */ 11 | export class LoopbackTransport implements ITransport { 12 | private readonly messageEmitter = new EventEmitter(); 13 | private readonly endEmitter = new EventEmitter(); 14 | private readonly didSendEmitter = new EventEmitter(); 15 | 16 | public readonly onMessage = this.messageEmitter.addListener; 17 | public readonly onEnd = this.endEmitter.addListener; 18 | 19 | /** 20 | * Fires when send() is called. 21 | */ 22 | public readonly onDidSend = this.didSendEmitter.addListener; 23 | 24 | /** 25 | * Causes `onMessage` to fire, as if a message was received. 26 | */ 27 | public receive(message: Transportable) { 28 | this.messageEmitter.emit(message); 29 | } 30 | 31 | /** 32 | * Causes `onEnd` to fire, optionally with an error. 33 | */ 34 | public endWith(error?: Error) { 35 | this.endEmitter.emit(error); 36 | } 37 | 38 | /** @inheritdoc */ 39 | public send(message: Transportable): void { 40 | this.didSendEmitter.emit(message); 41 | } 42 | 43 | /** @inheritdoc */ 44 | public dispose(): void { 45 | // no-op 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/agent/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { SpatialDocumentImpl } from '../../living/nodes/SpatialDocument'; 2 | import * as xsmlParser from './xsml'; 3 | 4 | type parseFragment = (markup: string, contextElement: HTMLElement) => void; 5 | type parseIntoDocument = (markup: string, ownerDocument: SpatialDocumentImpl) => void; 6 | 7 | export function parseFragment(markup: string, contextElement: HTMLElement) { 8 | // TODO 9 | } 10 | 11 | export function parseIntoDocument(markup: string, ownerDocument: SpatialDocumentImpl) { 12 | const { _parsingMode } = ownerDocument; 13 | 14 | let parseAlgorithm: parseIntoDocument; 15 | if (_parsingMode === 'xsml' || _parsingMode === 'html') { 16 | parseAlgorithm = xsmlParser.parseIntoDocument; 17 | } else { 18 | throw new Error('Unrecognized parsing mode'); 19 | } 20 | return parseAlgorithm(markup, ownerDocument); 21 | } 22 | -------------------------------------------------------------------------------- /src/agent/parser/xml-utils.ts: -------------------------------------------------------------------------------- 1 | export function toNode(xmlObject: any) { 2 | const node: Partial<{ 3 | name: string; 4 | text: string; 5 | attrs: { [key: string]: any }; 6 | children: any[]; 7 | }> = {}; 8 | 9 | let setTimes = 0; 10 | const keys = Object.keys(xmlObject); 11 | keys.forEach(key => { 12 | if (key === ':@') { 13 | node.attrs = xmlObject[key]; 14 | } else if (key === '#text') { 15 | node.name = key; 16 | node.text = xmlObject[key]; 17 | } else { 18 | node.name = key; 19 | node.children = xmlObject[key]; 20 | setTimes += 1; 21 | } 22 | }); 23 | if (setTimes > 1) { 24 | throw new TypeError('Invalid XML object, which has more than attributes and children members, the object keys are: ' + JSON.stringify(keys)); 25 | } 26 | 27 | if (!node.attrs) { 28 | node.attrs = {}; 29 | } 30 | return node; 31 | } 32 | -------------------------------------------------------------------------------- /src/agent/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/src/agent/resources/.gitkeep -------------------------------------------------------------------------------- /src/input-event.ts: -------------------------------------------------------------------------------- 1 | export enum HandGesture { 2 | None = -1, 3 | Grip = 1, 4 | Palm, 5 | Pinch, 6 | OpenPinch, 7 | } 8 | 9 | export enum HandOrientation { 10 | Up = 0, 11 | Down, 12 | Left, 13 | Right, 14 | } 15 | 16 | export type HandtrackingInputDetail = { 17 | handId: number; 18 | joints: Array<{ 19 | position: DOMPointInit; 20 | rotation: DOMPointInit; 21 | }>; 22 | pose?: { 23 | position: DOMPointInit; 24 | rotation: DOMPointInit; 25 | }; 26 | gesture?: HandGesture; 27 | orientation?: HandOrientation; 28 | }; 29 | 30 | export type RaycastInputDetail = { 31 | sourceId: string; 32 | sourceType?: 'hand' | 'head' | 'gamepad' | 'mouse' | 'custom'; 33 | targetSpatialElementInternalGuid: number; 34 | uvCoord: BABYLON.Nullable; 35 | }; 36 | 37 | export type RaycastActionInputDetail = { 38 | type: 'up' | 'down' | 'click'; 39 | sourceId: RaycastInputDetail['sourceId']; 40 | }; 41 | 42 | export type JSARInputDetail = HandtrackingInputDetail | RaycastInputDetail | RaycastActionInputDetail; 43 | 44 | export class JSARInputEvent extends Event { 45 | constructor(subType: 'handtracking', detail: HandtrackingInputDetail); 46 | constructor(subType: 'raycast', detail: RaycastInputDetail); 47 | constructor(subType: 'raycast_action', detail: RaycastActionInputDetail); 48 | constructor(public subType: 'handtracking' | 'raycast' | 'raycast_action', public detail: JSARInputDetail) { 49 | super('input'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/live2/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/src/live2/.gitkeep -------------------------------------------------------------------------------- /src/live3/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/M-CreativeLab/jsar-dom/07eca70b6ad4b23e8cc686679de39eaf39560f97/src/live3/.gitkeep -------------------------------------------------------------------------------- /src/living/audiocontext/Audio.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import HTMLAudioElementImpl from '../nodes/HTMLAudioElement'; 3 | 4 | export function createAudioConstructor(hostObject: NativeDocument) { 5 | return class Audio extends HTMLAudioElementImpl { 6 | constructor(src?: string) { 7 | super(hostObject, []); 8 | this.src = src; 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/living/compatible.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from '@jest/globals'; 2 | import { NodeImpl } from './nodes/Node'; 3 | 4 | type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y 5 | ? 1 6 | : 2 7 | ? true 8 | : false; 9 | 10 | type Expect = T; 11 | 12 | type FindValidFields = { 13 | [K in keyof T]: T[K] extends never ? never : K; 14 | }[keyof T]; 15 | 16 | type CheckStrictCompatible< 17 | StandardType, 18 | ImplType extends StandardType, 19 | Checked = { 20 | [k in keyof StandardType]: Equal extends true 21 | ? never 22 | : [StandardType[k], ImplType[k]]; 23 | } 24 | > = Pick>; 25 | 26 | /** 27 | * Check the consistency between the jsar-dom implementation and the standard dom implementation, 28 | * return the incompatible fields. 29 | */ 30 | type IncompatibleFields = CheckStrictCompatible; 31 | 32 | type cases = [Expect>]; 33 | 34 | /** 35 | * placeholder for jest, DO NOT REMOVE IT. 36 | */ 37 | it.todo('placeholder test'); 38 | -------------------------------------------------------------------------------- /src/living/crypto/Noise.ts: -------------------------------------------------------------------------------- 1 | import * as noise from '@bindings/noise'; 2 | 3 | export default class NoiseImpl { 4 | constructor(public seed?: number) { 5 | if (!seed) { 6 | this.seed = Math.random(); 7 | } 8 | } 9 | 10 | private _simplexN(inputs: number[]): number { 11 | const simplex = new noise.Simplex(this.seed); 12 | const point = new Float64Array(inputs); 13 | const v = simplex.get(point); 14 | simplex.free(); 15 | return v; 16 | } 17 | 18 | private _perlinN(inputs: number[]): number { 19 | const perlin = new noise.Perlin(this.seed); 20 | const point = new Float64Array(inputs); 21 | const v = perlin.get(point); 22 | perlin.free(); 23 | return v; 24 | } 25 | 26 | simplex2(x: number, y: number): number { 27 | return this._simplexN([x, y]); 28 | } 29 | 30 | simplex3(x: number, y: number, z: number): number { 31 | return this._simplexN([x, y, z]); 32 | } 33 | 34 | simplex4(x: number, y: number, z: number, w: number): number { 35 | return this._simplexN([x, y, z, w]); 36 | } 37 | 38 | perlin2(x: number, y: number): number { 39 | return this._perlinN([x, y]); 40 | } 41 | 42 | perlin3(x: number, y: number, z: number): number { 43 | return this._perlinN([x, y, z]); 44 | } 45 | 46 | perlin4(x: number, y: number, z: number, w: number): number { 47 | return this._perlinN([x, y, z, w]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/living/cssom/CSSRuleList.ts: -------------------------------------------------------------------------------- 1 | export default class CSSRuleListImpl implements CSSRuleList { 2 | _rules: CSSRule[] = []; 3 | 4 | [index: number]: CSSRule; 5 | get length(): number { 6 | return this._rules.length; 7 | } 8 | 9 | item(index: number): CSSRule { 10 | return this._rules[index]; 11 | } 12 | 13 | /** 14 | * @internal 15 | */ 16 | _add(rule: CSSRule) { 17 | this._rules.push(rule); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/living/cssom/CSSSpatialKeyframeRule.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import { parseCss, css, isComment } from '../helpers/spatial-css-parser'; 3 | import CSSRuleImpl from './CSSRule'; 4 | import CSSSpatialStyleDeclaration from './CSSSpatialStyleDeclaration'; 5 | 6 | export default class CSSSpatialKeyframeRule extends CSSRuleImpl { 7 | private _keyText: string; 8 | private _style: CSSSpatialStyleDeclaration = new CSSSpatialStyleDeclaration(); 9 | 10 | /** 11 | * @readonly 12 | */ 13 | get keyText(): string { 14 | return this._keyText; 15 | } 16 | /** 17 | * @readonly 18 | */ 19 | get style(): CSSSpatialStyleDeclaration { 20 | return this._style; 21 | } 22 | 23 | constructor( 24 | hostObject: NativeDocument, 25 | args: any[], 26 | privateData: { 27 | keyText: string; 28 | node: css.KeyFrame; 29 | ast: ReturnType; 30 | } 31 | ) { 32 | super(hostObject, args, { 33 | ...privateData.node, 34 | ast: privateData.ast, 35 | }); 36 | 37 | if (privateData.node) { 38 | this._keyText = privateData.keyText; 39 | this._initiateStyle(privateData.node.declarations); 40 | } 41 | } 42 | 43 | private _initiateStyle(decls: Array) { 44 | for (const decl of decls) { 45 | if (!isComment(decl)) { 46 | let priority = null; 47 | let value = decl.value; 48 | if (decl.value.endsWith(' !important')) { 49 | priority = 'important'; 50 | value = decl.value.slice(0, -' !important'.length); 51 | } 52 | this._style.setProperty(decl.property, value, priority); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/living/cssom/CSSSpatialStyleRule.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { css, isComment } from '../helpers/spatial-css-parser'; 3 | import CSSRuleImpl from './CSSRule'; 4 | import CSSSpatialStyleDeclaration from './CSSSpatialStyleDeclaration'; 5 | 6 | export default class CSSSpatialStyleRule extends CSSRuleImpl { 7 | selectorText: string; 8 | readonly style: CSSSpatialStyleDeclaration = new CSSSpatialStyleDeclaration(); 9 | readonly styleMap: StylePropertyMap; 10 | 11 | constructor( 12 | hostObject: NativeDocument, 13 | args: any[], 14 | privateData: css.Rule & ConstructorParameters[2] 15 | ) { 16 | super(hostObject, args, privateData); 17 | 18 | if (privateData) { 19 | this.selectorText = privateData.selectors.join(','); 20 | this._initiateStyle(privateData.declarations); 21 | } 22 | } 23 | 24 | _initiateStyle(decls: Array) { 25 | for (const decl of decls) { 26 | if (!isComment(decl)) { 27 | let priority = null; 28 | let value = decl.value; 29 | if (decl.value.endsWith(' !important')) { 30 | priority = 'important'; 31 | value = decl.value.slice(0, -' !important'.length); 32 | } 33 | this.style.setProperty(decl.property, value, priority); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/living/cssom/CSSStyleRule.ts: -------------------------------------------------------------------------------- 1 | import cssstyle from 'cssstyle'; 2 | import { NativeDocument } from '../../impl-interfaces'; 3 | import { css, isComment } from '../helpers/spatial-css-parser'; 4 | import CSSRuleImpl from './CSSRule'; 5 | 6 | export default class CSSStyleRuleImpl extends CSSRuleImpl implements CSSStyleRule { 7 | cssRules: CSSRuleList; 8 | selectorText: string; 9 | readonly style: CSSStyleDeclaration = new cssstyle.CSSStyleDeclaration() as unknown as CSSStyleDeclaration; 10 | readonly styleMap: StylePropertyMap; 11 | 12 | constructor( 13 | hostObject: NativeDocument, 14 | args: any[], 15 | privateData: css.Rule & ConstructorParameters[2] 16 | ) { 17 | super(hostObject, args, privateData); 18 | 19 | if (privateData) { 20 | this.selectorText = privateData.selectors.join(','); 21 | this._initiateStyle(privateData.declarations); 22 | } 23 | } 24 | 25 | _initiateStyle(decls: Array) { 26 | for (const decl of decls) { 27 | if (!isComment(decl)) { 28 | let priority = null; 29 | let value = decl.value; 30 | if (decl.value.endsWith(' !important')) { 31 | priority = 'important'; 32 | value = decl.value.slice(0, -' !important'.length); 33 | } 34 | this.style.setProperty(decl.property, value, priority); 35 | } 36 | } 37 | } 38 | 39 | deleteRule(index: number): void { 40 | throw new Error('The method "CSSStyleRule.prototype.deleteRule()" not implemented.'); 41 | } 42 | insertRule(rule: string, index?: number): number { 43 | throw new Error('The method "CSSStyleRule.prototype.insertRule()" not implemented.'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/living/cssom/StyleSheet.ts: -------------------------------------------------------------------------------- 1 | export default class StyleSheetImpl implements StyleSheet { 2 | disabled: boolean; 3 | href: string; 4 | media: MediaList; 5 | ownerNode: Element | ProcessingInstruction; 6 | parentStyleSheet: CSSStyleSheet = null; 7 | title: string; 8 | type: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/living/cssom/StyleSheetList.ts: -------------------------------------------------------------------------------- 1 | export default class StyleSheetListImpl extends Array implements StyleSheetList { 2 | constructor() { 3 | super(); 4 | } 5 | 6 | item(index: number): CSSStyleSheet { 7 | const result = this[index]; 8 | return result !== undefined ? result : null; 9 | } 10 | 11 | /** 12 | * @internal 13 | */ 14 | _add(sheet: CSSStyleSheet) { 15 | if (!this.includes(sheet)) { 16 | this.push(sheet); 17 | } 18 | } 19 | 20 | /** 21 | * @internal 22 | */ 23 | _remove(sheet: CSSStyleSheet) { 24 | const index = this.indexOf(sheet); 25 | if (index >= 0) { 26 | this.splice(index, 1); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/Error.ts: -------------------------------------------------------------------------------- 1 | export class $Error extends Error { 2 | public readonly code: string; 3 | public constructor(code: string, message?: string) { 4 | super(message ? `${code}: ${message}` : code); 5 | this.code = code; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/README.md: -------------------------------------------------------------------------------- 1 | # @hookun/parse-animation-shorthand 2 | 3 | This source is from the project [@hookun/parse-animation-shorthand](https://github.com/hookhookun/parse-animation-shorthand), under Apache 2.0 license. 4 | 5 | > The reason that we copied the source code is that the distrubution files(.mjs) from npm is not compatible working with our build system, thus we have 6 | > to copy the source code and build it by ourselves. 7 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/fillAnimation.ts: -------------------------------------------------------------------------------- 1 | import { CSSAnimation } from './type'; 2 | 3 | const defaults: Omit = { 4 | duration: 'unset', 5 | delay: 'unset', 6 | timingFunction: 'unset', 7 | iterationCount: 'unset', 8 | direction: 'unset', 9 | fillMode: 'unset', 10 | playState: 'unset', 11 | }; 12 | 13 | export const fillAnimation = (patch: Partial & { name: string }): CSSAnimation => ({ ...defaults, ...patch }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getCubicBezier.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { getCubicBezier } from './getCubicBezier'; 3 | 4 | const test = ( 5 | input: string, 6 | start: number, 7 | expected: string | { 8 | start: number, 9 | end: number, 10 | value: [number, number, number, number], 11 | }, 12 | ): void => { 13 | it(`${input} ${start} -> ${expected ? JSON.stringify(expected) : 'Error'}`, function () { 14 | if (typeof expected == 'string') { 15 | expect(() => getCubicBezier(input, start)).toThrowError(expected); 16 | } else { 17 | const result = getCubicBezier(input, start); 18 | expect(result).toEqual({ 19 | start: expected.start, 20 | end: expected.end, 21 | value: { 22 | type: 'cubic-bezier', 23 | value: expected.value, 24 | }, 25 | }); 26 | } 27 | }); 28 | }; 29 | 30 | describe('getCubicBezier', () => { 31 | test('(0,0,1,1)', 0, { start: 0, end: 9, value: [0, 0, 1, 1] }); 32 | test('( 0.1 , 0.2 , 0.3 , 0.4 )', 0, { start: 0, end: 25, value: [0.1, 0.2, 0.3, 0.4] }); 33 | test('( 0.68, -0.82, 0.42, 1.52 )', 0, { start: 0, end: 27, value: [0.68, -0.82, 0.42, 1.52] }); 34 | test('( 0.1 , 0.2 , 0.3 , 0.4 ', 0, 'UnclosedParenthesis'); 35 | test(' 0.1 , 0.2 , 0.3 , 0.4 )', 0, 'NoOpenParenthesis'); 36 | test('(0,0,1 1)', 0, 'NoComma'); 37 | }); 38 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getCubicBezier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isWhiteSpace, 3 | Comma, 4 | CloseParenthesis, 5 | OpenParenthesis, 6 | } from './character'; 7 | import { skip } from './skip'; 8 | import { getNumber } from './getNumber'; 9 | import { CSSCubicBezier } from './type'; 10 | import { $Error as Error } from './Error'; 11 | 12 | export const getCubicBezier = ( 13 | input: string, 14 | start: number, 15 | ): { start: number, end: number, value: CSSCubicBezier } => { 16 | if (input.codePointAt(start) !== OpenParenthesis) { 17 | throw new Error('NoOpenParenthesis'); 18 | } 19 | const value: [number, number, number, number] = [0, 0, 0, 0]; 20 | let end = start + 1; 21 | for (let index = 0; index < 4; index++) { 22 | end = skip(input, end, isWhiteSpace); 23 | if (0 < index) { 24 | if (input.codePointAt(end) === Comma) { 25 | end = skip(input, end + 1, isWhiteSpace); 26 | } else { 27 | throw new Error('NoComma'); 28 | } 29 | } 30 | const number = getNumber(input, end); 31 | value[index] = number.value; 32 | end = number.end; 33 | } 34 | end = skip(input, end, isWhiteSpace); 35 | if (input.codePointAt(end) !== CloseParenthesis) { 36 | throw new Error('UnclosedParenthesis'); 37 | } 38 | return { 39 | start, 40 | end: end + 1, 41 | value: { type: 'cubic-bezier', value }, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getCustomIdent.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { getCustomIdent } from './getCustomIdent'; 3 | 4 | const testGetCustomIdent = ( 5 | input: string, 6 | start: number, 7 | expected: null | { 8 | start: number, 9 | end: number, 10 | value: string, 11 | }, 12 | ): void => { 13 | test(`"${input}" ${start} should return ${expected ? JSON.stringify(expected) : 'error'}`, () => { 14 | if (expected) { 15 | expect(getCustomIdent(input, start)).toEqual(expected); 16 | } else { 17 | expect(() => getCustomIdent(input, start)).toThrow(); 18 | } 19 | }); 20 | }; 21 | 22 | testGetCustomIdent('"xyz"', 0, null); 23 | testGetCustomIdent('xyz', 0, { start: 0, end: 3, value: 'xyz' }); 24 | testGetCustomIdent('-xyz', 0, { start: 0, end: 4, value: '-xyz' }); 25 | testGetCustomIdent('--xyz', 0, { start: 0, end: 5, value: '--xyz' }); 26 | testGetCustomIdent('_xyz', 0, { start: 0, end: 4, value: '_xyz' }); 27 | testGetCustomIdent('_xyz\\?', 0, { start: 0, end: 6, value: '_xyz?' }); 28 | testGetCustomIdent('_xyz\\3F', 0, { start: 0, end: 7, value: '_xyz?' }); 29 | testGetCustomIdent('_xyz\\3f', 0, { start: 0, end: 7, value: '_xyz?' }); 30 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getCustomIdent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isIdentCharacter, 3 | Backslash, 4 | isHexCharacter, 5 | } from './character'; 6 | import { skip } from './skip'; 7 | import { $Error as Error } from './Error'; 8 | 9 | export const getCustomIdent = ( 10 | input: string, 11 | start: number, 12 | ): { start: number, end: number, value: string } => { 13 | const value = getCustomIdentOrNull(input, start); 14 | if (value) { 15 | return value; 16 | } 17 | throw new Error('NoIdent', [input.slice(0, start), input.slice(start)].join('→')); 18 | }; 19 | 20 | export const getCustomIdentOrNull = ( 21 | input: string, 22 | start: number, 23 | ): { start: number, end: number, value: string } | null => { 24 | let end = start; 25 | const fragments: Array = []; 26 | while (1) { 27 | const fragmentStart = end; 28 | end = skip(input, end, isIdentCharacter); 29 | fragments.push(input.slice(fragmentStart, end)); 30 | if (input.codePointAt(end) === Backslash) { 31 | end += 1; 32 | const hexEnd = skip(input, end, isHexCharacter); 33 | if (end < hexEnd) { 34 | const cp = Number.parseInt(input.slice(end, hexEnd), 16); 35 | fragments.push(String.fromCodePoint(cp)); 36 | end = hexEnd; 37 | } else { 38 | fragments.push(input.charAt(end)); 39 | end += 1; 40 | } 41 | } else { 42 | break; 43 | } 44 | } 45 | const value = fragments.join(''); 46 | if (value) { 47 | return { start, end, value }; 48 | } 49 | return null; 50 | }; 51 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { getNumber } from './getNumber'; 3 | 4 | const testGetNumber = ( 5 | input: string, 6 | start: number, 7 | expected: null | { 8 | start: number, 9 | end: number, 10 | value: number, 11 | }, 12 | ): void => { 13 | test(`"${input}" ${start} should return ${expected ? JSON.stringify(expected) : 'error'}`, () => { 14 | if (expected) { 15 | expect(getNumber(input, start)).toEqual(expected); 16 | } else { 17 | expect(() => getNumber(input, start)).toThrow(); 18 | } 19 | }); 20 | }; 21 | 22 | testGetNumber('12.34', 0, { start: 0, end: 5, value: 12.34 }); 23 | testGetNumber('-12.34', 0, { start: 0, end: 6, value: -12.34 }); 24 | testGetNumber('0.123', 0, { start: 0, end: 5, value: 0.123 }); 25 | testGetNumber('.123', 0, { start: 0, end: 4, value: 0.123 }); 26 | testGetNumber('0.0.123', 0, null); 27 | testGetNumber('00.123', 0, null); 28 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getNumber.ts: -------------------------------------------------------------------------------- 1 | import { isNumberStart } from './character'; 2 | import { skip } from './skip'; 3 | import { $Error as Error } from './Error'; 4 | 5 | export const getNumber = ( 6 | input: string, 7 | start: number, 8 | ): { start: number, end: number, value: number } => { 9 | const end = skip(input, start, isNumberStart); 10 | const literal = input.slice(start, end); 11 | const parts = literal.split('.'); 12 | if (parts.length <= 2 && /^-?(?:[1-9]\d*|0)?$/.test(parts[0])) { 13 | return { 14 | start, 15 | end, 16 | value: Number(literal), 17 | }; 18 | } 19 | throw new Error('InvalidNumber', literal); 20 | }; 21 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getSteps.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { getSteps } from './getSteps'; 3 | import { CSSStepDirection } from './type'; 4 | 5 | const testGetSteps = ( 6 | input: string, 7 | start: number, 8 | expected: string | { 9 | start: number, 10 | end: number, 11 | count: number, 12 | direction: string, 13 | }, 14 | ): void => { 15 | test(`"${input}" ${start} should return ${typeof expected === 'string' ? 'error' : JSON.stringify(expected)}`, () => { 16 | if (typeof expected === 'string') { 17 | expect(() => getSteps(input, start)).toThrowError(expected); 18 | } else { 19 | expect(getSteps(input, start)).toEqual({ 20 | start: expected.start, 21 | end: expected.end, 22 | value: { 23 | type: 'steps', 24 | stepCount: expected.count, 25 | direction: expected.direction as CSSStepDirection, 26 | }, 27 | }); 28 | } 29 | }); 30 | }; 31 | 32 | testGetSteps('(2,jump-start)', 0, { start: 0, end: 14, count: 2, direction: 'jump-start' }); 33 | testGetSteps('( 4 , jump-both )', 0, { start: 0, end: 17, count: 4, direction: 'jump-both' }); 34 | testGetSteps('( 4 , jump-bo )', 0, 'UnknownStepDirection'); 35 | testGetSteps('( 4 , jump-both ', 0, 'UnclosedParenthesis'); 36 | testGetSteps(' 4 , jump-both )', 0, 'NoOpenParenthesis'); 37 | testGetSteps('( 4 jump-both )', 0, 'UnclosedParenthesis'); 38 | testGetSteps('( 10 )', 0, { start: 0, end: 6, count: 10, direction: 'end' }); 39 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getSteps.ts: -------------------------------------------------------------------------------- 1 | import { getNumber } from './getNumber'; 2 | import { getCustomIdent } from './getCustomIdent'; 3 | import { 4 | isWhiteSpace, 5 | Comma, 6 | CloseParenthesis, 7 | OpenParenthesis, 8 | } from './character'; 9 | import { skip } from './skip'; 10 | import { StepDirection } from './keyword'; 11 | import { CSSSteps, CSSStepDirection } from './type'; 12 | import { $Error as Error } from './Error'; 13 | 14 | export const getSteps = ( 15 | input: string, 16 | start: number, 17 | ): { start: number, end: number, value: CSSSteps } => { 18 | if (input.codePointAt(start) !== OpenParenthesis) { 19 | throw new Error('NoOpenParenthesis'); 20 | } 21 | let end = skip(input, start + 1, isWhiteSpace); 22 | const count = getNumber(input, end); 23 | const value: CSSSteps = { 24 | type: 'steps', 25 | stepCount: count.value, 26 | direction: 'end', 27 | }; 28 | end = skip(input, count.end, isWhiteSpace); 29 | if (input.codePointAt(end) === Comma) { 30 | end = skip(input, end + 1, isWhiteSpace); 31 | const direction = getCustomIdent(input, end); 32 | if (!StepDirection.has(direction.value)) { 33 | throw new Error('UnknownStepDirection', direction.value); 34 | } 35 | value.direction = direction.value as CSSStepDirection; 36 | end = direction.end; 37 | } 38 | end = skip(input, end, isWhiteSpace); 39 | if (input.codePointAt(end) !== CloseParenthesis) { 40 | throw new Error('UnclosedParenthesis'); 41 | } 42 | return { start, end: end + 1, value }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getString.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { getString } from './getString'; 3 | 4 | const testGetString = ( 5 | input: string, 6 | start: number, 7 | expected: string | { 8 | start: number, 9 | end: number, 10 | value: string, 11 | }, 12 | ): void => { 13 | test(`"${input}" ${start} should return ${typeof expected === 'string' ? 'error' : JSON.stringify(expected)}`, () => { 14 | if (typeof expected === 'string') { 15 | expect(() => getString(input, start)).toThrowError(expected); 16 | } else { 17 | expect(getString(input, start)).toEqual(expected); 18 | } 19 | }); 20 | }; 21 | 22 | testGetString('"abc"', 0, { start: 0, end: 5, value: 'abc' }); 23 | testGetString('abc', 0, 'InvalidQuote'); 24 | testGetString('"abc', 0, 'UnterminatedString'); 25 | testGetString('"abc\\"def"', 0, { start: 0, end: 10, value: 'abc"def' }); 26 | testGetString('\'abc\'', 0, { start: 0, end: 5, value: 'abc' }); 27 | testGetString('\'abc', 0, 'UnterminatedString'); 28 | testGetString('\'abc\\"def\'', 0, { start: 0, end: 10, value: 'abc"def' }); 29 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/getString.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DoubleQuote, 3 | SingleQuote, 4 | Backslash, 5 | isNot, 6 | } from './character'; 7 | import { skip } from './skip'; 8 | import { $Error as Error } from './Error'; 9 | 10 | export const getString = ( 11 | input: string, 12 | start: number, 13 | ): { start: number, end: number, value: string } => { 14 | const quote = input.codePointAt(start); 15 | if (quote !== DoubleQuote && quote !== SingleQuote) { 16 | throw new Error('InvalidQuote', input[start]); 17 | } 18 | const isNotQuoteOrBackslash = isNot(quote, Backslash); 19 | const fragments: Array = []; 20 | let end = start + 1; 21 | while (1) { 22 | const fragmentStart = end; 23 | end = skip(input, end, isNotQuoteOrBackslash); 24 | fragments.push(input.slice(fragmentStart, end)); 25 | const charCode = input.codePointAt(end); 26 | if (charCode === Backslash) { 27 | fragments.push(input.charAt(end + 1)); 28 | end += 2; 29 | } else if (charCode === quote) { 30 | return { 31 | start, 32 | end: end + 1, 33 | value: fragments.join(''), 34 | }; 35 | } else { 36 | break; 37 | } 38 | } 39 | throw new Error('UnterminatedString'); 40 | }; 41 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { parse } from './index'; 3 | 4 | const testParse = ( 5 | input: string, 6 | expected: ReturnType, 7 | ): void => { 8 | test(`${input} should parse as ${JSON.stringify(expected)}`, () => { 9 | expect(parse(input)).toEqual(expected); 10 | }); 11 | }; 12 | 13 | testParse( 14 | 'play1 .8s steps(10) infinite, play2 .8s steps(10) infinite', 15 | [ 16 | { 17 | duration: 800, 18 | delay: 'unset', 19 | timingFunction: { 20 | direction: 'end', 21 | stepCount: 10, 22 | type: 'steps', 23 | }, 24 | iterationCount: 'infinite', 25 | direction: 'unset', 26 | fillMode: 'unset', 27 | playState: 'unset', 28 | name: 'play1', 29 | }, 30 | { 31 | duration: 800, 32 | delay: 'unset', 33 | timingFunction: { 34 | direction: 'end', 35 | stepCount: 10, 36 | type: 'steps', 37 | }, 38 | iterationCount: 'infinite', 39 | direction: 'unset', 40 | fillMode: 'unset', 41 | playState: 'unset', 42 | name: 'play2', 43 | }, 44 | ] 45 | ); 46 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/index.ts: -------------------------------------------------------------------------------- 1 | export * from './type'; 2 | import {parseSingleAnimationShorthand} from './parseSingleAnimationShorthand'; 3 | export const parseSingle = parseSingleAnimationShorthand; 4 | import {parseAnimationShorthand} from './parseAnimationShorthand'; 5 | export const parse = (input: string) => [...parseAnimationShorthand(input)]; 6 | import {serializeAnimation} from './serializeAnimation'; 7 | export const serialize = serializeAnimation; 8 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/keyword.ts: -------------------------------------------------------------------------------- 1 | export const StepDirection = new Set([ 2 | 'jump-start', 3 | 'jump-end', 4 | 'jump-none', 5 | 'jump-both', 6 | 'start', 7 | 'end', 8 | ]); 9 | 10 | export const TimingFunctionKeyword = new Set([ 11 | 'ease', 12 | 'ease-in', 13 | 'ease-out', 14 | 'ease-in-out', 15 | 'linear', 16 | 'step-start', 17 | 'step-end', 18 | ]); 19 | 20 | export const AnimationDirection = new Set([ 21 | 'normal', 22 | 'reverse', 23 | 'alternate', 24 | 'alternate-reverse', 25 | ]); 26 | 27 | export const AnimationFillMode = new Set([ 28 | 'none', 29 | 'forwards', 30 | 'backwards', 31 | 'both', 32 | ]); 33 | 34 | export const AnimationPlayState = new Set([ 35 | 'paused', 36 | 'running', 37 | ]); 38 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/parseAnimationShorthand.ts: -------------------------------------------------------------------------------- 1 | import { CSSAnimation } from './type'; 2 | import { parseSingleAnimationShorthand } from './parseSingleAnimationShorthand'; 3 | import { isWhiteSpace, Comma } from './character'; 4 | import { skip } from './skip'; 5 | 6 | export const parseAnimationShorthand = function* ( 7 | input: string, 8 | ): Generator { 9 | let start = 0; 10 | while (start < input.length) { 11 | const result = parseSingleAnimationShorthand(input, start); 12 | yield result.value; 13 | start = result.end; 14 | start = skip(input, start, isWhiteSpace); 15 | if (input.codePointAt(start) === Comma) { 16 | start = skip(input, start + 1, isWhiteSpace); 17 | } else { 18 | break; 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeAnimation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { serializeAnimation } from './serializeAnimation'; 3 | import { CSSAnimation } from './type'; 4 | 5 | const testSerializeAnimation = ( 6 | input: Partial & { name: string }, 7 | expected: string, 8 | ): void => { 9 | test(`${JSON.stringify(input)} should serialize to ${expected}`, () => { 10 | expect(serializeAnimation(input)).toBe(expected); 11 | }); 12 | }; 13 | 14 | testSerializeAnimation({ name: 'abc' }, 'abc'); 15 | testSerializeAnimation( 16 | { 17 | name: 'abc', 18 | duration: 200, 19 | timingFunction: 'ease-in-out', 20 | }, 21 | '.2s ease-in-out abc' 22 | ); 23 | testSerializeAnimation( 24 | { 25 | name: 'abc', 26 | duration: 200, 27 | delay: 50, 28 | iterationCount: 'infinite', 29 | timingFunction: 'ease-in-out', 30 | playState: 'running', 31 | fillMode: 'forwards', 32 | }, 33 | '.2s 50ms ease-in-out infinite forwards running abc' 34 | ); 35 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeAnimation.ts: -------------------------------------------------------------------------------- 1 | import { CSSAnimation } from './type'; 2 | import { fillAnimation } from './fillAnimation'; 3 | import { serializeAnimationValue } from './serializeAnimationValue'; 4 | 5 | export const serializeAnimation = (animation: Partial & { name: string }): string => { 6 | const result: Array = []; 7 | const filled = fillAnimation(animation); 8 | for (const key of Object.keys(filled) as Array) { 9 | const serialized = serializeAnimationValue(key, filled[key]); 10 | if (serialized) { 11 | result.push(serialized); 12 | } 13 | } 14 | return result.join(' '); 15 | }; 16 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeAnimationValue.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { serializeAnimationValue } from './serializeAnimationValue'; 3 | import { CSSAnimation } from './type'; 4 | 5 | const testSerializeAnimationValue = ( 6 | key: Key, 7 | value: CSSAnimation[Key], 8 | expected: string, 9 | ): void => { 10 | test(`${key}: ${value} should serialize to ${expected}`, () => { 11 | expect(serializeAnimationValue(key, value)).toBe(expected); 12 | }); 13 | }; 14 | 15 | testSerializeAnimationValue('name', 'foo', 'foo'); 16 | testSerializeAnimationValue('duration', 1, '1ms'); 17 | testSerializeAnimationValue('duration', 10, '10ms'); 18 | testSerializeAnimationValue('duration', 100, '.1s'); 19 | testSerializeAnimationValue('duration', 1000, '1s'); 20 | testSerializeAnimationValue('delay', 1, '1ms'); 21 | testSerializeAnimationValue('delay', 10, '10ms'); 22 | testSerializeAnimationValue('delay', 100, '.1s'); 23 | testSerializeAnimationValue('delay', 1000, '1s'); 24 | testSerializeAnimationValue('iterationCount', 10, '10'); 25 | testSerializeAnimationValue('iterationCount', 'unset', ''); 26 | testSerializeAnimationValue( 27 | 'timingFunction', 28 | 'ease-in', 29 | 'ease-in' 30 | ); 31 | testSerializeAnimationValue( 32 | 'timingFunction', 33 | { type: 'steps', stepCount: 3, direction: 'jump-both' }, 34 | 'steps(3,jump-both)' 35 | ); 36 | testSerializeAnimationValue( 37 | 'timingFunction', 38 | { type: 'cubic-bezier', value: [0.01, 0.1, 0.7, 1.0] }, 39 | 'cubic-bezier(.01,.1,.7,1)' 40 | ); 41 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeAnimationValue.ts: -------------------------------------------------------------------------------- 1 | import { CSSAnimation, CSSCubicBezier, CSSSteps } from './type'; 2 | import { serializeNumber } from './serializeNumber'; 3 | import { shortest } from './shortest'; 4 | 5 | export const serializeAnimationValue = ( 6 | key: Key, 7 | value: CSSAnimation[Key], 8 | ): string => { 9 | if (value === 'unset') { 10 | return ''; 11 | } 12 | if (typeof value === 'object') { 13 | const timingFunction = value as CSSCubicBezier | CSSSteps; 14 | if (timingFunction.type === 'cubic-bezier') { 15 | return `cubic-bezier(${timingFunction.value.map(serializeNumber).join(',')})`; 16 | } 17 | return `steps(${timingFunction.stepCount},${timingFunction.direction})`; 18 | } 19 | if (typeof value === 'number') { 20 | switch (key) { 21 | case 'duration': 22 | case 'delay': 23 | return shortest( 24 | `${serializeNumber(value)}ms`, 25 | `${serializeNumber(value * 0.001)}s`, 26 | ); 27 | default: 28 | return serializeNumber(value); 29 | } 30 | } 31 | return `${value}`; 32 | }; 33 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { serializeNumber } from './serializeNumber'; 3 | 4 | test('serializeNumber should serialize 0 to "0"', () => { 5 | expect(serializeNumber(0)).toBe('0'); 6 | }); 7 | 8 | test('serializeNumber should serialize 1 to "1"', () => { 9 | expect(serializeNumber(1)).toBe('1'); 10 | }); 11 | 12 | test('serializeNumber should serialize 1.1 to "1.1"', () => { 13 | expect(serializeNumber(1.1)).toBe('1.1'); 14 | }); 15 | 16 | test('serializeNumber should serialize 0.010 to ".01"', () => { 17 | expect(serializeNumber(0.010)).toBe('.01'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/serializeNumber.ts: -------------------------------------------------------------------------------- 1 | export const serializeNumber = (value: number): string => { 2 | const parts = value.toFixed(3).split('.'); 3 | const decimal = parts[1].replace(/0+$/, ''); 4 | if (decimal) { 5 | const integer = parts[0].replace(/^0/, ''); 6 | return `${integer}.${decimal}`; 7 | } 8 | return parts[0]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/shortest.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { shortest } from './shortest'; 3 | 4 | test('shortest function should return the shortest string when given multiple strings', () => { 5 | expect(shortest('a', 'b', 'c', 'd')).toBe('a'); 6 | expect(shortest('aa', 'b', 'c', 'd')).toBe('b'); 7 | expect(shortest('aa', 'bb', 'c', 'd')).toBe('c'); 8 | expect(shortest('aa', 'bb', 'cc', 'd')).toBe('d'); 9 | expect(shortest('aa', 'bb', 'cc', 'dd')).toBe('aa'); 10 | }); 11 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/shortest.ts: -------------------------------------------------------------------------------- 1 | export const shortest = (...list: Array): string => { 2 | const listLength = list.length; 3 | let result = list[0]; 4 | for (let index = 1; index < listLength; index++) { 5 | const candidate = list[index]; 6 | if (candidate.length < result.length) { 7 | result = candidate; 8 | } 9 | } 10 | return result; 11 | }; 12 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/skip.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { skip } from './skip'; 3 | import { CodePointTest } from './type'; 4 | import { isWhiteSpace } from './character'; 5 | 6 | describe('skip function', () => { 7 | it('should skip white spaces correctly', () => { 8 | const input = ' a'; 9 | const start = 0; 10 | const codePointTest = isWhiteSpace; 11 | const expected = 3; 12 | 13 | console.info(`${input} ${start} ${codePointTest.name} -> ${expected ? JSON.stringify(expected) : 'Error'}`); 14 | if (typeof expected === 'string') { 15 | expect(() => skip(input, start, codePointTest)).toThrowError({ code: expected }); 16 | } else { 17 | expect(skip(input, start, codePointTest)).toEqual(expected); 18 | } 19 | }); 20 | 21 | it('should skip pairs correctly', () => { 22 | const input = '🍎🍎🍎a'; 23 | const start = 0; 24 | const codePointTest: CodePointTest = (codePoint?: number): codePoint is number => { 25 | return typeof codePoint === 'number' && 0xFFFF < codePoint; 26 | }; 27 | const expected = 6; 28 | 29 | console.info(`${input} ${start} ${codePointTest.name} -> ${expected ? JSON.stringify(expected) : 'Error'}`); 30 | if (typeof expected === 'string') { 31 | expect(() => skip(input, start, codePointTest)).toThrowError({ code: expected }); 32 | } else { 33 | expect(skip(input, start, codePointTest)).toEqual(expected); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/skip.ts: -------------------------------------------------------------------------------- 1 | import { CodePointTest } from './type'; 2 | 3 | export const skip = ( 4 | input: string, 5 | start: number, 6 | test: CodePointTest, 7 | ): number => { 8 | let end = start; 9 | const inputLength = input.length; 10 | while (end <= inputLength) { 11 | const cp = input.codePointAt(end); 12 | if (!test(cp)) { 13 | break; 14 | } 15 | end += 0xFFFF < cp ? 2 : 1; 16 | } 17 | return end; 18 | }; 19 | -------------------------------------------------------------------------------- /src/living/cssom/parsers/animation-shorthand/type.ts: -------------------------------------------------------------------------------- 1 | export interface CodePointTest { 2 | (cp?: number): cp is number, 3 | } 4 | 5 | export interface CSSCubicBezier { 6 | type: 'cubic-bezier', 7 | value: [number, number, number, number], 8 | } 9 | 10 | export type CSSStepDirection = 11 | | 'jump-start' 12 | | 'jump-end' 13 | | 'jump-none' 14 | | 'jump-both' 15 | | 'start' 16 | | 'end'; 17 | 18 | export interface CSSSteps { 19 | type: 'steps', 20 | stepCount: number, 21 | direction: CSSStepDirection, 22 | } 23 | 24 | export type CSSTimingFunctionKeyword = 25 | | 'ease' 26 | | 'ease-in' 27 | | 'ease-out' 28 | | 'ease-in-out' 29 | | 'linear' 30 | | 'step-start' 31 | | 'step-end'; 32 | 33 | export type CSSTimingFunction = CSSCubicBezier | CSSSteps | CSSTimingFunctionKeyword; 34 | 35 | export type CSSAnimationDirection = 36 | | 'normal' 37 | | 'reverse' 38 | | 'alternate' 39 | | 'alternate-reverse'; 40 | 41 | export type CSSAnimationFillMode = 42 | | 'none' 43 | | 'forwards' 44 | | 'backwards' 45 | | 'both'; 46 | 47 | export type CSSAnimationPlayState = 48 | | 'paused' 49 | | 'running'; 50 | 51 | export interface CSSAnimation { 52 | name: string, 53 | duration: number | 'unset', 54 | timingFunction: CSSTimingFunction | 'unset', 55 | delay: number | 'unset', 56 | iterationCount: number | 'infinite' | 'unset', 57 | direction: CSSAnimationDirection | 'unset', 58 | fillMode: CSSAnimationFillMode | 'unset', 59 | playState: CSSAnimationPlayState | 'unset', 60 | } 61 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/albedo-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('albedo-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('albedo-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/albedo-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('albedo-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('albedo-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/ambient-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('ambient-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('ambient-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/ambient-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('ambient-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('ambient-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/animation-duration.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toTimespanStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('animation-duration'); 9 | }, 10 | set(value: string | number) { 11 | this._setProperty('animation-duration', toTimespanStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/animation-iteration-count.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue, toTimespanStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('animation-iteration-count'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('animation-iteration-count', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/animation-name.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('animation-name'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('animation-name', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/animation.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { shorthandGetter, shorthandSetter, type ShorthandFor } from '../parsers'; 3 | 4 | import AnimationName from './animation-name'; 5 | import AnimationDuration from './animation-duration'; 6 | import AnimationIterationCount from './animation-iteration-count'; 7 | 8 | const shorthandFor: ShorthandFor = { 9 | 'animation-name': AnimationName, 10 | 'animation-duration': AnimationDuration, 11 | 'animation-iteration-count': AnimationIterationCount, 12 | }; 13 | 14 | export default defineSpatialProperty({ 15 | enumerable: true, 16 | configurable: true, 17 | get: shorthandGetter('animation', shorthandFor), 18 | set: shorthandSetter('animation', shorthandFor), 19 | }); 20 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/bump-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('bump-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('bump-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/diffuse-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('diffuse-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('diffuse-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/diffuse-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('diffuse-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('diffuse-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/emissive-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('emissive-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('emissive-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/emissive-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('emissive-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('emissive-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/helper.ts: -------------------------------------------------------------------------------- 1 | import type CSSSpatialStyleDeclaration from '../CSSSpatialStyleDeclaration'; 2 | 3 | type SpatialPropertyDescriptor = Omit & { 4 | get?(this: CSSSpatialStyleDeclaration): string; 5 | set?(this: CSSSpatialStyleDeclaration, value: string): void; 6 | }; 7 | 8 | type Extras = { 9 | isValid: (v: any, ...extra: any[]) => boolean; 10 | }; 11 | 12 | export function defineSpatialProperty( 13 | descriptor: SpatialPropertyDescriptor, 14 | extra?: Partial 15 | ): SpatialPropertyDescriptor & Extras { 16 | const combinedDescriptor: SpatialPropertyDescriptor & Extras = { 17 | ...descriptor, 18 | isValid: () => true, 19 | }; 20 | if (typeof extra?.isValid === 'function') { 21 | combinedDescriptor.isValid = extra.isValid; 22 | } 23 | return combinedDescriptor; 24 | } 25 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-alpha-mode.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-orientation'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-orientation', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-background-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-background-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-background-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-cell-height.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toLengthStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-cell-height'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-cell-height', toLengthStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-cell-width.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toLengthStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-cell-width'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-cell-width', toLengthStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-height.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toLengthStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-height'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-height', toLengthStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-major-line-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-major-line-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-major-line-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-minor-line-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-minor-line-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-minor-line-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-grid-width.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toLengthStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-grid-width'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-grid-width', toLengthStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-orientation.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-orientation'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-orientation', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material-type.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material-type'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material-type', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/material.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toStringStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('material'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('material', toStringStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/physical-metallic.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('physical-metallic'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('physical-metallic', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/physical-roughness.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('physical-roughness'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('physical-roughness', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/position-x.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('x'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('x', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/position-y.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('y'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('y', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/position-z.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('z'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('z', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/rotation.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { 3 | CSSValueType, 4 | valueType, 5 | implicitSetter, 6 | toAngleStr, 7 | PropertyValue, 8 | } from '../parsers'; 9 | 10 | function rotationValidator(v): boolean { 11 | if (v.toLowerCase() === 'auto') { 12 | return true; 13 | } 14 | const type = valueType(v); 15 | return ( 16 | type === CSSValueType.ANGLE || 17 | type === CSSValueType.INTEGER 18 | ); 19 | } 20 | 21 | function toRotationStr(v: string) { 22 | v = v.toLowerCase(); 23 | if (v === 'auto') { 24 | return PropertyValue.createString(v); 25 | } 26 | return toAngleStr(v); 27 | } 28 | 29 | // TODO: support quaternion(x, y, z, w)? 30 | const parts = ['x', 'y', 'z']; 31 | const valueSetter = implicitSetter( 32 | 'rotation', 33 | '', 34 | parts, 35 | rotationValidator, 36 | toRotationStr 37 | ); 38 | const globalSetter = implicitSetter( 39 | 'rotation', 40 | '', 41 | parts, 42 | () => true, 43 | (v) => v 44 | ); 45 | 46 | export default defineSpatialProperty({ 47 | enumerable: true, 48 | configurable: true, 49 | get(): string { 50 | return this.getPropertyValue('rotation'); 51 | }, 52 | set(value: string) { 53 | if (typeof value === 'number') { 54 | value = String(value); 55 | } 56 | if (typeof value !== 'string') { 57 | return; 58 | } 59 | 60 | const _v = value.trim().toLowerCase(); 61 | switch (_v) { 62 | case 'inherit': 63 | case 'initial': 64 | case 'unset': 65 | case '': 66 | globalSetter.call(this, _v); 67 | break; 68 | default: 69 | valueSetter.call(this, _v); 70 | break; 71 | } 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/scaling.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { 3 | CSSValueType, 4 | valueType, 5 | PropertyValue, 6 | implicitSetter, 7 | toNumberStr, 8 | toIntegerStr, 9 | } from '../parsers'; 10 | 11 | function scalingValidator(v): boolean { 12 | switch (valueType(v)) { 13 | case CSSValueType.NUMBER: 14 | case CSSValueType.INTEGER: 15 | return true; 16 | default: 17 | return false; 18 | } 19 | } 20 | 21 | function toScalingStr(v: string) { 22 | const type = valueType(v); 23 | if (type === CSSValueType.NUMBER) { 24 | return toNumberStr(v); 25 | } else if (type === CSSValueType.INTEGER) { 26 | return toIntegerStr(v); 27 | } 28 | } 29 | 30 | const valueSetter = implicitSetter( 31 | 'scaling', 32 | '', 33 | ['x', 'y', 'z'], 34 | scalingValidator, 35 | toScalingStr 36 | ); 37 | const globalSetter = implicitSetter( 38 | 'scaling', 39 | '', 40 | ['x', 'y', 'z'], 41 | () => true, 42 | (v) => PropertyValue.createKeyword(v), 43 | ); 44 | 45 | export default defineSpatialProperty({ 46 | enumerable: true, 47 | configurable: true, 48 | get(): string { 49 | return this.getPropertyValue('scaling'); 50 | }, 51 | set(value: string) { 52 | if (typeof value === 'number') { 53 | value = String(value); 54 | } 55 | if (typeof value !== 'string') { 56 | return; 57 | } 58 | 59 | const _v = value.trim().toLowerCase(); 60 | switch (_v) { 61 | case 'inherit': 62 | case 'initial': 63 | case 'unset': 64 | case '': 65 | globalSetter.call(this, _v); 66 | break; 67 | default: 68 | valueSetter.call(this, _v); 69 | break; 70 | } 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/specular-color.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toColorStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('specular-color'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('specular-color', toColorStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/specular-power.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toNumberStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('specular-power'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('specular-power', toNumberStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/specular-texture.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { toUrlStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('specular-texture'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('specular-texture', toUrlStr(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/spatial-properties/wireframe.ts: -------------------------------------------------------------------------------- 1 | import { defineSpatialProperty } from './helper'; 2 | import { PropertyValue, toStringStr } from '../parsers'; 3 | 4 | export default defineSpatialProperty({ 5 | enumerable: true, 6 | configurable: true, 7 | get(): string { 8 | return this.getPropertyValue('wireframe'); 9 | }, 10 | set(value: string) { 11 | this._setProperty('wireframe', PropertyValue.createKeyword(value), null); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/cssom/utils/color-space.ts: -------------------------------------------------------------------------------- 1 | // https://www.w3.org/TR/css-color-4/#hsl-to-rgb 2 | /** 3 | * @param {number} hue - Hue as degrees 0..360 4 | * @param {number} sat - Saturation as percentage 0..100 5 | * @param {number} light - Lightness as percentage 0..100 6 | * @return {number[]} Array of RGB components 0..255 7 | */ 8 | export function hslToRgb(hue: number, sat: number, light: number): [number, number, number] { 9 | hue = hue % 360; 10 | 11 | if (hue < 0) { 12 | hue += 360; 13 | } 14 | 15 | sat /= 100; 16 | light /= 100; 17 | 18 | function f(n: number): number { 19 | const k = (n + hue / 30) % 12; 20 | const a = sat * Math.min(light, 1 - light); 21 | const v = light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); 22 | return Math.round(v * 255); 23 | } 24 | 25 | return [f(0), f(8), f(4)]; 26 | } 27 | -------------------------------------------------------------------------------- /src/living/domexception.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import DOMExceptionImpl from './domexception'; 3 | 4 | describe('DOMExceptionImpl', () => { 5 | it('should create a DOMException with the correct code', () => { 6 | const message = 'Test message'; 7 | const name = 'INDEX_SIZE_ERR'; 8 | const exception = new DOMExceptionImpl(message, name); 9 | 10 | expect(exception.message).toBe(message); 11 | expect(exception.name).toBe(name); 12 | expect(exception.code).toBe(DOMExceptionImpl[name]); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/living/domparsing/InnerHTML.ts: -------------------------------------------------------------------------------- 1 | import { HTML_NS } from '../helpers/namespaces'; 2 | import { isShadowRoot } from '../helpers/shadow-dom'; 3 | import { ElementImpl } from '../nodes/Element'; 4 | import { NodeImpl } from '../nodes/Node'; 5 | import { fragmentSerialization } from './serialization'; 6 | 7 | // https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin 8 | export default class InnerHTMLImpl implements InnerHTML { 9 | // https://w3c.github.io/DOM-Parsing/#dom-innerhtml-innerhtml 10 | get innerHTML() { 11 | if (!(this instanceof ElementImpl)) { 12 | throw new TypeError("innerHTML is only implemented on Element"); 13 | } 14 | 15 | return fragmentSerialization(this, { 16 | outer: false, 17 | requireWellFormed: true, 18 | globalObject: this._hostObject, 19 | }); 20 | } 21 | 22 | set innerHTML(markup: string) { 23 | if (!(this instanceof ElementImpl)) { 24 | throw new TypeError('innerHTML is only implemented on Element'); 25 | } 26 | 27 | const contextElement = isShadowRoot(this) ? this._host : this; 28 | // const fragment = parseFragment(markup, contextElement); 29 | 30 | // let contextObject = this; 31 | // if (this.nodeType === NodeImpl.ELEMENT_NODE && this.localName === 'template' && this.namespaceURI === HTML_NS) { 32 | // contextObject = this._templateContents; 33 | // } 34 | // contextObject._replaceAll(fragment); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/living/domparsing/serialization.ts: -------------------------------------------------------------------------------- 1 | import { NodeImpl } from "../nodes/Node"; 2 | import { SpatialDocumentImpl } from "../nodes/SpatialDocument"; 3 | 4 | type SerializationOptions = { 5 | outer: boolean; 6 | requireWellFormed: boolean; 7 | globalObject: any; 8 | }; 9 | 10 | export function fragmentSerialization(node: NodeImpl, options: SerializationOptions) { 11 | let contextDocument: SpatialDocumentImpl; 12 | if (node.nodeType === NodeImpl.DOCUMENT_NODE) { 13 | contextDocument = node as SpatialDocumentImpl; 14 | } else { 15 | contextDocument = node._ownerDocument 16 | } 17 | 18 | if (contextDocument._parsingMode === 'html') { 19 | // TODO 20 | } 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /src/living/esm-supports.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": { 3 | "script": [ 4 | ".ts", 5 | ".mjs", 6 | ".js" 7 | ], 8 | "json": [ 9 | ".json" 10 | ], 11 | "arraybuffer": [ 12 | ".bin", 13 | ".data", 14 | ".wasm", 15 | ".png", 16 | ".jpg", 17 | ".jpeg", 18 | ".webp", 19 | ".mp3", 20 | ".wav", 21 | ".ogg" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/living/events/CloseEvent.ts: -------------------------------------------------------------------------------- 1 | export class CloseEventImpl extends Event implements CloseEvent { 2 | code: number; 3 | reason: string; 4 | wasClean: boolean; 5 | 6 | constructor(type: string, options?: CloseEventInit) { 7 | super(type, options); 8 | 9 | this.code = options?.code || 0; 10 | this.reason = options.reason || ''; 11 | this.wasClean = options.wasClean || false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/living/events/CustomEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export class CustomEventImpl extends Event implements CustomEvent { 4 | detail: any; 5 | constructor(type: string, options?: CustomEventInit) { 6 | super(type, options); 7 | 8 | this.detail = options?.detail || null; 9 | } 10 | 11 | initCustomEvent(type: string, bubbles?: boolean, cancelable?: boolean, detail?: any): void { 12 | throw new DOMException('CustomEvent.initCustomEvent() has been deprecated.'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/living/events/ErrorEvent.ts: -------------------------------------------------------------------------------- 1 | export default class ErrorEventImpl extends Event implements ErrorEvent { 2 | message: string; 3 | filename: string; 4 | lineno: number; 5 | colno: number; 6 | error: Error; 7 | 8 | constructor(type: string, eventInitDict?: ErrorEventInit) { 9 | super(type, eventInitDict); 10 | this.message = eventInitDict?.message || ''; 11 | this.filename = eventInitDict?.filename || ''; 12 | this.lineno = eventInitDict?.lineno || 0; 13 | this.colno = eventInitDict?.colno || 0; 14 | this.error = eventInitDict?.error || null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/living/events/FocusEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export default class FocusEventImpl extends Event implements FocusEvent { 4 | relatedTarget: EventTarget; 5 | detail: number; 6 | view: Window; 7 | which: number; 8 | inputIndex: number; 9 | 10 | constructor(type: string, eventInitDict?: FocusEventInit) { 11 | super(type, eventInitDict); 12 | this.relatedTarget = eventInitDict?.relatedTarget || null; 13 | } 14 | 15 | initUIEvent(typeArg: string, bubblesArg?: boolean, cancelableArg?: boolean, viewArg?: Window, detailArg?: number): void { 16 | throw new DOMException('FocusEvent.initUIEvent() has been deprecated.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/living/events/HandTrackingEvent.ts: -------------------------------------------------------------------------------- 1 | import { HandGesture, HandOrientation, HandtrackingInputDetail } from '../../input-event'; 2 | 3 | enum HandType { 4 | Left = 0, 5 | Right, 6 | } 7 | 8 | type InputData = { 9 | 'Type': HandType; 10 | 'Joints': Array<{ 11 | position: DOMPointInit; 12 | rotation: DOMPointInit; 13 | }>; 14 | 'ThisPose': any; 15 | 'Gesture': HandGesture; 16 | 'Orientation': HandOrientation; 17 | }; 18 | 19 | export default class HandTrackingEvent extends Event { 20 | inputData: InputData; 21 | 22 | constructor(inputDetail: HandtrackingInputDetail) { 23 | super('handtracking'); 24 | this.inputData = { 25 | 'Type': inputDetail.handId, 26 | 'Joints': inputDetail.joints, 27 | 'ThisPose': inputDetail.pose, 28 | 'Gesture': inputDetail.gesture, 29 | 'Orientation': inputDetail.orientation, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/living/events/HashChangeEvent.ts: -------------------------------------------------------------------------------- 1 | export default class HashChangeEventImpl extends Event implements HashChangeEvent { 2 | oldURL: string; 3 | newURL: string; 4 | 5 | constructor(type: string, eventInitDict?: HashChangeEventInit) { 6 | super(type, eventInitDict); 7 | this.oldURL = eventInitDict?.oldURL || ''; 8 | this.newURL = eventInitDict?.newURL || ''; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/living/events/MessageEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export default class MessageEventImpl extends Event implements MessageEvent { 4 | data: any; 5 | lastEventId: string; 6 | origin: string; 7 | ports: readonly MessagePort[]; 8 | source: MessageEventSource; 9 | 10 | constructor(type: string, eventInitDict?: MessageEventInit) { 11 | super(type, eventInitDict); 12 | this.data = eventInitDict?.data || null; 13 | this.lastEventId = eventInitDict?.lastEventId || ''; 14 | this.origin = eventInitDict?.origin || ''; 15 | this.ports = eventInitDict?.ports || []; 16 | this.source = eventInitDict?.source || null; 17 | } 18 | 19 | initMessageEvent(type: string, bubbles?: boolean, cancelable?: boolean, data?: any, origin?: string, lastEventId?: string, source?: MessageEventSource, ports?: MessagePort[]): void; 20 | initMessageEvent(type: string, bubbles?: boolean, cancelable?: boolean, data?: any, origin?: string, lastEventId?: string, source?: MessageEventSource, ports?: Iterable): void; 21 | initMessageEvent(type: unknown, bubbles?: unknown, cancelable?: unknown, data?: unknown, origin?: unknown, lastEventId?: unknown, source?: unknown, ports?: unknown): void { 22 | throw new DOMException('MessageEvent.initMessageEvent() has been deprecated.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/living/events/PopStateEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export default class PopStateEventImpl extends Event implements PopStateEvent { 4 | state: any; 5 | 6 | constructor(type: string, eventInitDict?: PopStateEventInit) { 7 | super(type, eventInitDict); 8 | this.state = eventInitDict?.state || null; 9 | } 10 | 11 | initPopStateEvent(typeArg: string, bubblesArg?: boolean, cancelableArg?: boolean, stateArg?: any): void { 12 | throw new DOMException('PopStateEvent.initPopStateEvent() has been deprecated.'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/living/events/ProgressEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export default class ProgressEventImpl extends Event implements ProgressEvent { 4 | lengthComputable: boolean; 5 | loaded: number; 6 | total: number; 7 | 8 | constructor(type: string, eventInitDict?: ProgressEventInit) { 9 | super(type, eventInitDict); 10 | this.lengthComputable = eventInitDict?.lengthComputable || false; 11 | this.loaded = eventInitDict?.loaded || 0; 12 | this.total = eventInitDict?.total || 0; 13 | } 14 | 15 | initProgressEvent(typeArg: string, bubblesArg?: boolean, cancelableArg?: boolean, lengthComputableArg?: boolean, loadedArg?: number, totalArg?: number): void { 16 | throw new DOMException('ProgressEvent.initProgressEvent() has been deprecated.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/living/events/TouchEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | import { UIEventImpl } from './UIEvent'; 3 | 4 | export default class TouchEventImpl extends UIEventImpl implements TouchEvent { 5 | touches: TouchList; 6 | targetTouches: TouchList; 7 | changedTouches: TouchList; 8 | altKey: boolean; 9 | metaKey: boolean; 10 | ctrlKey: boolean; 11 | shiftKey: boolean; 12 | 13 | constructor(type: string, eventInitDict?: TouchEventInit) { 14 | super(type, eventInitDict); 15 | this.touches = null; 16 | this.targetTouches = null; 17 | this.changedTouches = null; 18 | this.altKey = eventInitDict?.altKey || false; 19 | this.metaKey = eventInitDict?.metaKey || false; 20 | this.ctrlKey = eventInitDict?.ctrlKey || false; 21 | this.shiftKey = eventInitDict?.shiftKey || false; 22 | } 23 | 24 | initTouchEvent(typeArg: string, bubblesArg?: boolean, cancelableArg?: boolean, viewArg?: Window, detailArg?: number, ctrlKeyArg?: boolean, altKeyArg?: boolean, shiftKeyArg?: boolean, metaKeyArg?: boolean, touchesArg?: TouchList, targetTouchesArg?: TouchList, changedTouchesArg?: TouchList): void { 25 | throw new DOMException('TouchEvent.initTouchEvent() has been deprecated.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/living/events/UIEvent.ts: -------------------------------------------------------------------------------- 1 | import DOMException from 'domexception'; 2 | 3 | export class UIEventImpl extends Event implements UIEvent { 4 | detail: number; 5 | view: Window; 6 | which: number; 7 | inputIndex: number; 8 | shdaowRoot: ShadowRoot | null; 9 | 10 | constructor(typeArg: string, eventInitDict?: UIEventInit & { shadowRoot?: ShadowRoot }) { 11 | super(typeArg, eventInitDict); 12 | this.detail = eventInitDict?.detail || 0; 13 | this.shdaowRoot = eventInitDict?.shadowRoot || null; 14 | } 15 | 16 | initUIEvent(_typeArg: string, _bubblesArg?: boolean, _cancelableArg?: boolean, _viewArg?: Window, _detailArg?: number): void { 17 | throw new DOMException('This is a deprecated API. Please use UIEvent constructor instead.', 'NotSupportedError'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/living/geometry/DOMPointReadOnly.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import DOMPointReadOnlyImpl from './DOMPointReadOnly'; 3 | 4 | describe('DOMPointReadOnly', () => { 5 | it('creates a DOMPointReadOnly', () => { 6 | const point = new DOMPointReadOnlyImpl(); 7 | expect(point.x).toBe(0); 8 | expect(point.y).toBe(0); 9 | expect(point.z).toBe(0); 10 | expect(point.w).toBe(1); 11 | }); 12 | 13 | it('creates a DOMPointReadOnly from specific parameters', () => { 14 | const point = new DOMPointReadOnlyImpl(1, 0, 1, 0); 15 | expect(point.x).toBe(1); 16 | expect(point.y).toBe(0); 17 | expect(point.z).toBe(1); 18 | expect(point.w).toBe(0); 19 | }); 20 | 21 | it('supports the static method "fromPoint"', () => { 22 | const point = DOMPointReadOnlyImpl.fromPoint({ x: 100, y: 200, z: 0, w: 1 }); 23 | expect(point.x).toBe(100); 24 | expect(point.y).toBe(200); 25 | expect(point.z).toBe(0); 26 | expect(point.w).toBe(1); 27 | }); 28 | 29 | it('supports toJSON()', () => { 30 | const point = new DOMPointReadOnlyImpl(1, 0, 1, 0); 31 | expect(point.toJSON()).toStrictEqual({ 32 | x: 1, 33 | y: 0, 34 | z: 1, 35 | w: 0 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/living/geometry/DOMPointReadOnly.ts: -------------------------------------------------------------------------------- 1 | export default class DOMPointReadOnlyImpl implements DOMPointReadOnly { 2 | private _w: number; 3 | private _x: number; 4 | private _y: number; 5 | private _z: number; 6 | 7 | get x(): number { 8 | return this._x; 9 | } 10 | get y(): number { 11 | return this._y; 12 | } 13 | get z(): number { 14 | return this._z; 15 | } 16 | get w(): number { 17 | return this._w; 18 | } 19 | 20 | static fromPoint(sourcePoint: DOMPointInit): DOMPointReadOnly { 21 | return new DOMPointReadOnlyImpl(sourcePoint.x, sourcePoint.y, sourcePoint.z, sourcePoint.w); 22 | } 23 | 24 | constructor(x?: number, y?: number, z?: number, w?: number) { 25 | this._x = typeof x === 'number' ? x : 0; 26 | this._y = typeof y === 'number' ? y : 0; 27 | this._z = typeof z === 'number' ? z : 0; 28 | this._w = typeof w === 'number' ? w : 1; 29 | } 30 | 31 | matrixTransform(matrix?: DOMMatrixInit): DOMPoint { 32 | throw new Error('The method "DOMPointReadOnly.prototype.matrixTransform()" not implemented.'); 33 | } 34 | 35 | toJSON() { 36 | return { 37 | x: this._x, 38 | y: this._y, 39 | z: this._z, 40 | w: this._w, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/living/geometry/DOMRect.ts: -------------------------------------------------------------------------------- 1 | import DOMRectReadOnlyImpl from './DOMRectReadOnly'; 2 | 3 | export default class DOMRectImpl extends DOMRectReadOnlyImpl implements DOMRect { 4 | static fromRect(other?: DOMRectInit): DOMRect { 5 | return new DOMRectImpl(other?.x, other?.y, other?.width, other?.height); 6 | } 7 | 8 | get x() { 9 | return this._x; 10 | } 11 | set x(value: number) { 12 | this._x = value; 13 | } 14 | get y() { 15 | return this._y; 16 | } 17 | set y(value: number) { 18 | this._y = value; 19 | } 20 | get width() { 21 | return this._width; 22 | } 23 | set width(value: number) { 24 | this._width = value; 25 | } 26 | get height() { 27 | return this._height; 28 | } 29 | set height(value: number) { 30 | this._height = value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/living/geometry/DOMRectReadOnly.ts: -------------------------------------------------------------------------------- 1 | export default class DOMRectReadOnlyImpl implements DOMRectReadOnly { 2 | protected _x: number; 3 | protected _y: number; 4 | protected _width: number; 5 | protected _height: number; 6 | 7 | static fromRect(other?: DOMRectInit): DOMRectReadOnly { 8 | if (other) { 9 | return new DOMRectReadOnlyImpl(other.x, other.y, other.width, other.height); 10 | } else { 11 | return new DOMRectReadOnlyImpl(); 12 | } 13 | } 14 | 15 | constructor(x?: number, y?: number, width?: number, height?: number) { 16 | this._x = x; 17 | this._y = y; 18 | this._width = width; 19 | this._height = height; 20 | } 21 | 22 | get x() { 23 | return this._x; 24 | } 25 | get y() { 26 | return this._y; 27 | } 28 | get width() { 29 | return this._width; 30 | } 31 | get height() { 32 | return this._height; 33 | } 34 | get top() { 35 | return Math.min(this._y, this._y + this._height); 36 | } 37 | get right() { 38 | return Math.max(this._x, this._x + this._width); 39 | } 40 | get bottom() { 41 | return Math.max(this._y, this._y + this._height); 42 | } 43 | get left() { 44 | return Math.min(this._x, this._x + this._width); 45 | } 46 | get [Symbol.toStringTag]() { 47 | return 'DOMRectReadOnly'; 48 | } 49 | 50 | toJSON() { 51 | return { 52 | x: this._x, 53 | y: this._y, 54 | width: this._width, 55 | height: this._height, 56 | top: this.top, 57 | right: this.right, 58 | bottom: this.bottom, 59 | left: this.left, 60 | }; 61 | } 62 | } -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/OBJ/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mtlFileLoader'; 2 | export * from './objLoadingOptions'; 3 | export * from './solidParser'; 4 | export * from './objFileLoader'; 5 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/OBJ/objLoadingOptions.ts: -------------------------------------------------------------------------------- 1 | type Vector2 = BABYLON.Vector2; 2 | 3 | /** 4 | * Options for loading OBJ/MTL files 5 | */ 6 | export type OBJLoadingOptions = { 7 | /** 8 | * Defines if UVs are optimized by default during load. 9 | */ 10 | optimizeWithUV: boolean; 11 | /** 12 | * Defines custom scaling of UV coordinates of loaded meshes. 13 | */ 14 | // eslint-disable-next-line @typescript-eslint/naming-convention 15 | UVScaling: Vector2; 16 | /** 17 | * Invert model on y-axis (does a model scaling inversion) 18 | */ 19 | invertY: boolean; 20 | /** 21 | * Invert Y-Axis of referenced textures on load 22 | */ 23 | invertTextureY: boolean; 24 | /** 25 | * Include in meshes the vertex colors available in some OBJ files. This is not part of OBJ standard. 26 | */ 27 | importVertexColors: boolean; 28 | /** 29 | * Compute the normals for the model, even if normals are present in the file. 30 | */ 31 | computeNormals: boolean; 32 | /** 33 | * Optimize the normals for the model. Lighting can be uneven if you use OptimizeWithUV = true because new vertices can be created for the same location if they pertain to different faces. 34 | * Using OptimizehNormals = true will help smoothing the lighting by averaging the normals of those vertices. 35 | */ 36 | optimizeNormals: boolean; 37 | /** 38 | * Skip loading the materials even if defined in the OBJ file (materials are ignored). 39 | */ 40 | skipMaterials: boolean; 41 | /** 42 | * When a material fails to load OBJ loader will silently fail and onSuccess() callback will be triggered. 43 | */ 44 | materialLoadingFailsSilently: boolean; 45 | }; 46 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Babylon.js Loaders 2 | 3 | In this directory, we moved https://github.com/BabylonJS/Babylon.js/tree/6.10.0/packages/dev/loaders with a change to use global `BABYLON` to reduce the bundle size. 4 | 5 | ## Plan 6 | 7 | 1. Use WebAssembly to speed up the loading and parsing these formats. 8 | 2. Supports USD format for Apple Vision Pro. 9 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/STL/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stlFileLoader'; 2 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/gLTF/1.0/index.ts: -------------------------------------------------------------------------------- 1 | export * from './glTFBinaryExtension'; 2 | export * from './glTFLoader'; 3 | export * from './glTFLoaderInterfaces'; 4 | export * from './glTFLoaderUtils'; 5 | export * from './glTFMaterialsCommonExtension'; 6 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/gLTF/2.0/Extensions/KHR_mesh_quantization.ts: -------------------------------------------------------------------------------- 1 | import type { IGLTFLoaderExtension } from '../glTFLoaderExtension'; 2 | import { GLTFLoader } from '../glTFLoader'; 3 | 4 | const NAME = 'KHR_mesh_quantization'; 5 | 6 | /** 7 | * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md) 8 | */ 9 | // eslint-disable-next-line @typescript-eslint/naming-convention 10 | export class KHR_mesh_quantization implements IGLTFLoaderExtension { 11 | /** 12 | * The name of this extension. 13 | */ 14 | public readonly name = NAME; 15 | 16 | /** 17 | * Defines whether this extension is enabled. 18 | */ 19 | public enabled: boolean; 20 | 21 | /** 22 | * @internal 23 | */ 24 | constructor(loader: GLTFLoader) { 25 | this.enabled = loader.isExtensionUsed(NAME); 26 | } 27 | 28 | /** @internal */ 29 | public dispose() { } 30 | } 31 | 32 | GLTFLoader.RegisterExtension(NAME, (loader) => new KHR_mesh_quantization(loader)); 33 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/gLTF/2.0/Extensions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EXT_lights_image_based'; 2 | export * from './EXT_mesh_gpu_instancing'; 3 | export * from './EXT_meshopt_compression'; 4 | export * from './EXT_texture_webp'; 5 | export * from './KHR_draco_mesh_compression'; 6 | export * from './KHR_lights_punctual'; 7 | export * from './KHR_materials_pbrSpecularGlossiness'; 8 | export * from './KHR_materials_unlit'; 9 | export * from './KHR_materials_clearcoat'; 10 | export * from './KHR_materials_iridescence'; 11 | export * from './KHR_materials_anisotropy'; 12 | export * from './KHR_materials_emissive_strength'; 13 | export * from './KHR_materials_sheen'; 14 | export * from './KHR_materials_specular'; 15 | export * from './KHR_materials_ior'; 16 | export * from './KHR_materials_variants'; 17 | export * from './KHR_materials_transmission'; 18 | export * from './KHR_materials_translucency'; 19 | export * from './KHR_materials_volume'; 20 | export * from './KHR_mesh_quantization'; 21 | export * from './KHR_texture_basisu'; 22 | export * from './KHR_texture_transform'; 23 | export * from './KHR_xmp_json_ld'; 24 | export * from './KHR_animation_pointer'; 25 | export * from './MSFT_audio_emitter'; 26 | export * from './MSFT_lod'; 27 | export * from './MSFT_minecraftMesh'; 28 | export * from './MSFT_sRGBFactors'; 29 | export * from './ExtrasAsMetadata'; 30 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/gLTF/2.0/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-internal-modules */ 2 | export * from './glTFLoader'; 3 | export * from './glTFLoaderExtension'; 4 | export * from './glTFLoaderInterfaces'; 5 | export * from './Extensions/index'; 6 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/gLTF/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-internal-modules */ 2 | export * from './glTFFileLoader'; 3 | export * from './glTFValidation'; 4 | import * as GLTF1 from './1.0/index'; 5 | import * as GLTF2 from './2.0/index'; 6 | export { GLTF1, GLTF2 }; 7 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/loaders/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-internal-modules */ 2 | export * from './gLTF/index'; 3 | export * from './OBJ/index'; 4 | export * from './STL/index'; 5 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/patches/index.ts: -------------------------------------------------------------------------------- 1 | import './rewrite-meshopt-compression'; 2 | import './rewrite-draco-compression'; 3 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/patches/rewrite-meshopt-compression.ts: -------------------------------------------------------------------------------- 1 | import { MeshoptDecoder } from 'meshoptimizer'; 2 | 3 | (BABYLON.MeshoptCompression as any)._Default = { 4 | decodeGltfBufferAsync(source, count, stride, mode, filter) { 5 | return MeshoptDecoder.ready.then(() => { 6 | const result = new Uint8Array(count * stride); 7 | MeshoptDecoder.decodeGltfBuffer(result, count, stride, source, mode, filter); 8 | return result; 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/living/helpers/babylonjs/tags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This tag is used to identify the material that is created by the scss. 3 | */ 4 | export const MATERIAL_BY_SCSS = 'material-scss'; 5 | -------------------------------------------------------------------------------- /src/living/helpers/internal-constants.ts: -------------------------------------------------------------------------------- 1 | import SymbolTree from 'symbol-tree'; 2 | // import type { NodeImpl } from '../nodes/Node'; 3 | 4 | export const cloningSteps = Symbol('cloning steps'); 5 | export const domSymbolTree = new SymbolTree('DOM SymbolTree'); 6 | -------------------------------------------------------------------------------- /src/living/helpers/iterable-weak-set.ts: -------------------------------------------------------------------------------- 1 | export default class IterableWeakSet { 2 | _refSet = new Set>(); 3 | _refMap = new Map>(); 4 | _finalizationRegistry = new FinalizationRegistry(({ ref, set }) => set.delete(ref)); 5 | 6 | add(value: T): this { 7 | if (!this._refMap.has(value)) { 8 | const ref = new WeakRef(value); 9 | this._refMap.set(value, ref); 10 | this._refSet.add(ref); 11 | this._finalizationRegistry.register(value, { ref, set: this._refSet }, ref); 12 | } 13 | return this; 14 | } 15 | 16 | delete(value: T): boolean { 17 | const ref = this._refMap.get(value); 18 | if (!ref) { 19 | return false; 20 | } 21 | this._refMap.delete(value); 22 | this._refSet.delete(ref); 23 | this._finalizationRegistry.unregister(ref); 24 | return true; 25 | } 26 | 27 | has(value: T): boolean { 28 | return this._refMap.has(value); 29 | } 30 | 31 | *[Symbol.iterator]() { 32 | for (const ref of this._refSet) { 33 | const value = ref.deref(); 34 | if (value === undefined) { 35 | continue; 36 | } 37 | yield value; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/living/helpers/matrix-functions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import DOMMatrixImpl from '../geometry/DOMMatrix'; 3 | import { translate, rotate2d } from './matrix-functions'; 4 | 5 | const matrix: DOMMatrix = new DOMMatrixImpl([ 6 | 1, 0, 0, 0, 7 | 0, 1, 0, 0, 8 | 0, 0, 1, 0, 9 | 0, 0, 0, 1 10 | ]); 11 | 12 | describe('rotate2d', () => { 13 | it('should rotate2d the transform matrix correctly', () => { 14 | const angle = Math.random() * 360; 15 | const result = rotate2d(matrix, angle); 16 | const cosValue = Number(Math.cos(angle * Math.PI / 180).toFixed(2)); 17 | const sinValue = Number(Math.sin(angle * Math.PI / 180).toFixed(2)); 18 | const expectedMatrix: DOMMatrix = new DOMMatrixImpl([ 19 | cosValue, sinValue, 0, 0, 20 | -sinValue, cosValue, 0, 0, 21 | 0, 0, 1, 0, 22 | 0, 0, 0, 1 23 | ]); 24 | expect(result).toEqual(expectedMatrix); 25 | }); 26 | }); 27 | 28 | describe('translate', () => { 29 | it('should translate the transform matrix correctly', () => { 30 | const x = Math.random() * 100; 31 | const y = Math.random() * 100; 32 | const z = Math.random() * 100; 33 | const result = translate(matrix, x, y, z); 34 | const expectedMatrix: DOMMatrix = new DOMMatrixImpl([ 35 | 1, 0, 0, 0, 36 | 0, 1, 0, 0, 37 | 0, 0, 1, 0, 38 | x, y, z, 1 39 | ]); 40 | expect(result).toEqual(expectedMatrix); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/living/helpers/namespaces.ts: -------------------------------------------------------------------------------- 1 | // https://infra.spec.whatwg.org/#namespaces 2 | 3 | export const HTML_NS = 'http://www.w3.org/1999/xhtml'; 4 | export const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'; 5 | export const SVG_NS = 'http://www.w3.org/2000/svg'; 6 | export const XLINK_NS = 'http://www.w3.org/1999/xlink'; 7 | export const XML_NS = 'http://www.w3.org/XML/1998/namespace'; 8 | export const XMLNS_NS = 'http://www.w3.org/2000/xmlns/'; 9 | export const XSML_NS = 'http://jsar.netlify.app/spec/xsml'; 10 | -------------------------------------------------------------------------------- /src/living/helpers/scripting-types.ts: -------------------------------------------------------------------------------- 1 | export type ResolveContext = { 2 | conditions: string[]; 3 | importAttributes: object; 4 | parentURL: string | undefined; 5 | }; 6 | 7 | export type LoadContext = { 8 | conditions: string[]; 9 | importAttributes: object; 10 | format: string; 11 | }; 12 | 13 | export type ResolveResult = { 14 | format?: string | null | undefined; 15 | importAttributes?: object | undefined; 16 | shortCircuit?: boolean | undefined; 17 | url: string; 18 | }; 19 | 20 | export type LoadResult = { 21 | format: 'json' | 'binary' | 'module'; 22 | shortCircuit?: boolean; 23 | source: string | object | ArrayBuffer | Uint8Array; 24 | }; 25 | 26 | export type NextResolve = (specifier: string, context: ResolveContext) => ResolveResult; 27 | export type NextLoad = (url: string, context: LoadContext) => LoadResult; 28 | 29 | export type LoaderOnInitialize = (data: any) => void; 30 | export type LoaderOnResolve = (specifier: string, context: ResolveContext, nextResolve: NextResolve) => ResolveResult; 31 | export type LoaderOnLoad = (url: string, context: LoadContext, nextLoad: NextLoad) => LoadResult; 32 | export type CustomLoaderHooks = { 33 | initialize?: LoaderOnInitialize; 34 | resolve?: LoaderOnResolve; 35 | load?: LoaderOnLoad; 36 | }; 37 | 38 | export function getUrlFromResolveResult(resolved: ResolveResult): string { 39 | let url = resolved.url; 40 | if (resolved.url[0] === '/') { 41 | url = new URL(resolved.url, 'file://').href; 42 | } 43 | return url; 44 | } 45 | -------------------------------------------------------------------------------- /src/living/helpers/text.ts: -------------------------------------------------------------------------------- 1 | import { NodeImpl } from '../nodes/Node'; 2 | import { domSymbolTree } from './internal-constants'; 3 | 4 | export function childTextContent(node: NodeImpl) { 5 | let result = ''; 6 | const iterator = domSymbolTree.childrenIterator(node); 7 | for (const child of iterator) { 8 | if (child.nodeType === NodeImpl.TEXT_NODE || 9 | // The CDataSection extends Text. 10 | child.nodeType === NodeImpl.CDATA_SECTION_NODE) { 11 | result += child.data; 12 | } 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /src/living/helpers/url.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { isWin32 } from '../../utils'; 3 | 4 | /** 5 | * Checks if a given string can be parsed as a valid URL. 6 | * @param url - The string to be checked. 7 | * @returns True if the string can be parsed as a valid URL, false otherwise. 8 | */ 9 | export function canParseURL(url: string): boolean { 10 | /** 11 | * When the url is a file path with a drive latter, it can't be parsed as a URL. 12 | */ 13 | if (/^[a-zA-Z]:/.test(url)) { 14 | return false; 15 | } 16 | try { 17 | new URL(url); 18 | return true; 19 | } catch (_e) { 20 | return false; 21 | } 22 | } 23 | 24 | /** 25 | * Joins a subpath with a base path to create a new URL or file path. 26 | * If the base path is not an HTTP or HTTPS URL, it will be treated as a file path. 27 | * If the base path is an HTTP or HTTPS URL, the subpath will be appended to the URL's pathname. 28 | * @param sub - The subpath to join with the base path. 29 | * @param base - The base path to join with the subpath. 30 | * @returns The joined URL or file path. 31 | */ 32 | export function join(sub: string, base: string) { 33 | const isHttpOrHttps = base.startsWith('http://') || base.startsWith('https://'); 34 | if (!isHttpOrHttps) { 35 | return path.join(base, sub); 36 | } else { 37 | const url = new URL(base); 38 | url.pathname = path.join(url.pathname, sub); 39 | return url.href; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/living/image/ImageData.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import ImageDataImpl from './ImageData'; 3 | 4 | describe('ImageDataImpl', () => { 5 | it('should create a new instance with default settings', () => { 6 | // Arrange 7 | const data = new Uint8ClampedArray([255, 0, 0, 255]); 8 | const width = 100; 9 | const height = 200; 10 | 11 | // Act 12 | const imageData = new ImageDataImpl(data, width, height); 13 | 14 | // Assert 15 | expect(imageData.data).toBe(data); 16 | expect(imageData.width).toBe(width); 17 | expect(imageData.height).toBe(height); 18 | expect(imageData.colorSpace).toBe('srgb'); 19 | }); 20 | 21 | it('should create a new instance with custom settings', () => { 22 | // Arrange 23 | const data = new Uint8ClampedArray([255, 0, 0, 255]); 24 | const width = 100; 25 | const height = 200; 26 | const colorSpace = 'p3'; 27 | 28 | // Act 29 | const imageData = new ImageDataImpl(data, width, height, { colorSpace }); 30 | 31 | // Assert 32 | expect(imageData.data).toBe(data); 33 | expect(imageData.width).toBe(width); 34 | expect(imageData.height).toBe(height); 35 | expect(imageData.colorSpace).toBe(colorSpace); 36 | }); 37 | 38 | it('should create a new instance with null data', () => { 39 | // Arrange 40 | const width = 100; 41 | const height = 200; 42 | 43 | // Act 44 | const imageData = new ImageDataImpl(width, height); 45 | 46 | // Assert 47 | expect(imageData.data).toBeNull(); 48 | expect(imageData.width).toBe(width); 49 | expect(imageData.height).toBe(height); 50 | expect(imageData.colorSpace).toBe('srgb'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/living/image/ImageData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents image data used in the canvas element. 3 | */ 4 | export default class ImageDataImpl implements ImageData { 5 | private _data: Uint8ClampedArray; 6 | private _height: number; 7 | private _width: number; 8 | private _colorSpace: PredefinedColorSpace = 'srgb'; 9 | 10 | get data(): Uint8ClampedArray { 11 | return this._data; 12 | } 13 | get height(): number { 14 | return this._height; 15 | } 16 | get width(): number { 17 | return this._width; 18 | } 19 | get colorSpace(): PredefinedColorSpace { 20 | return this._colorSpace; 21 | } 22 | 23 | /** 24 | * Creates a new instance of ImageDataImpl. 25 | * @param data - The pixel data of the image. 26 | * @param sw - The width of the image. 27 | * @param sh - The height of the image. 28 | * @param settings - The settings for the image data. 29 | */ 30 | constructor(data: unknown, sw: unknown, sh?: unknown, settings?: unknown) { 31 | if (typeof data === 'number') { 32 | settings = sh as ImageDataSettings; 33 | sh = sw as number; 34 | sw = data as number; 35 | data = null; 36 | } 37 | 38 | this._data = data as Uint8ClampedArray; 39 | this._width = sw as number; 40 | this._height = sh as number || this._width; 41 | 42 | if (settings) { 43 | this._colorSpace = (settings as ImageDataSettings).colorSpace; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/living/interfaces.test.ts: -------------------------------------------------------------------------------- 1 | import { loadImplementations, getInterfaceWrapper } from './interfaces'; 2 | import { describe, it, expect } from '@jest/globals'; 3 | 4 | describe('interfaces', () => { 5 | it('should ensure that all modules are loaded', async() => { 6 | const isParallel = false; 7 | await loadImplementations(isParallel); 8 | 9 | expect(getInterfaceWrapper('NamedNodeMap').prototype.constructor.name).toBe('NamedNodeMapImpl'); 10 | expect(getInterfaceWrapper('XRSession').prototype.constructor.name).toBe('XRSessionImpl'); 11 | expect(getInterfaceWrapper('SpatialElement').prototype.constructor.name).toBe('SpatialElement'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/living/mutation-observer/MutationRecord.ts: -------------------------------------------------------------------------------- 1 | import { NodeListImpl } from '../nodes/NodeList'; 2 | 3 | // https://dom.spec.whatwg.org/#mutationrecord 4 | export class MutationRecordImpl implements MutationRecord { 5 | attributeName: string; 6 | attributeNamespace: string; 7 | nextSibling: Node; 8 | oldValue: string; 9 | previousSibling: Node; 10 | target: Node; 11 | type: MutationRecordType; 12 | 13 | _hostObject: any; 14 | _addedNodes: Node[]; 15 | _removedNodes: Node[]; 16 | 17 | constructor(hostObject, args, privateData) { 18 | this._hostObject = hostObject; 19 | 20 | this.type = privateData.type; 21 | this.target = privateData.target; 22 | this.previousSibling = privateData.previousSibling; 23 | this.nextSibling = privateData.nextSibling; 24 | this.attributeName = privateData.attributeName; 25 | this.attributeNamespace = privateData.attributeNamespace; 26 | this.oldValue = privateData.oldValue; 27 | 28 | this._addedNodes = privateData.addedNodes; 29 | this._removedNodes = privateData.removedNodes; 30 | } 31 | 32 | get addedNodes() { 33 | return new NodeListImpl(this._hostObject, [], { 34 | nodes: this._addedNodes, 35 | }); 36 | } 37 | 38 | get removedNodes() { 39 | return new NodeListImpl(this._hostObject, [], { 40 | nodes: this._removedNodes, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/living/node-document-position.ts: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | DOCUMENT_POSITION_DISCONNECTED: 1, 3 | DOCUMENT_POSITION_PRECEDING: 2, 4 | DOCUMENT_POSITION_FOLLOWING: 4, 5 | DOCUMENT_POSITION_CONTAINS: 8, 6 | DOCUMENT_POSITION_CONTAINED_BY: 16, 7 | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32 8 | }); 9 | -------------------------------------------------------------------------------- /src/living/nodes/DocumentFragment.ts: -------------------------------------------------------------------------------- 1 | import { domSymbolTree } from '../helpers/internal-constants'; 2 | import { NativeDocument } from '../../impl-interfaces'; 3 | 4 | import { NodeImpl } from './Node'; 5 | import ParentNodeImpl from './ParentNode'; 6 | import NonElementParentNodeImpl from './NonElementParentNode'; 7 | import { applyMixins } from '../../mixin'; 8 | 9 | export default interface DocumentFragmentImpl extends NodeImpl, NonElementParentNodeImpl, ParentNodeImpl { }; 10 | export default class DocumentFragmentImpl extends NodeImpl implements DocumentFragment { 11 | constructor( 12 | hostObject: NativeDocument, 13 | args, 14 | privateData: { 15 | host: NodeImpl, 16 | } 17 | ) { 18 | super(hostObject, args, { 19 | defaultView: hostObject.attachedDocument._defaultView, 20 | }); 21 | this.nodeType = NodeImpl.DOCUMENT_FRAGMENT_NODE; 22 | this._host = privateData.host; 23 | } 24 | 25 | getElementById(id: string): HTMLElement { 26 | if (id === '') { 27 | return null; 28 | } 29 | for (const descendant of domSymbolTree.treeIterator(this)) { 30 | if (descendant.nodeType === NodeImpl.ELEMENT_NODE && 31 | descendant.getAttributeNS(null, 'id') === id) { 32 | return descendant; 33 | } 34 | } 35 | return null; 36 | } 37 | } 38 | 39 | applyMixins(DocumentFragmentImpl, [NonElementParentNodeImpl, ParentNodeImpl]); 40 | -------------------------------------------------------------------------------- /src/living/nodes/DocumentType.ts: -------------------------------------------------------------------------------- 1 | import type { BaseWindowImpl } from '../../agent/window'; 2 | import { NativeDocument } from '../../impl-interfaces'; 3 | import { NodeImpl } from './Node'; 4 | 5 | export class DocumentTypeImpl extends NodeImpl implements DocumentType { 6 | name: string; 7 | publicId: string; 8 | systemId: string; 9 | 10 | constructor( 11 | hostObject: NativeDocument, 12 | _args, 13 | privateData: { 14 | name: string; 15 | publicId: string; 16 | systemId: string; 17 | defaultView: BaseWindowImpl; 18 | } 19 | ) { 20 | super(hostObject, [], { 21 | defaultView: privateData.defaultView, 22 | }); 23 | 24 | this.nodeType = this.DOCUMENT_TYPE_NODE; 25 | this.name = privateData.name; 26 | this.publicId = privateData.publicId; 27 | this.systemId = privateData.systemId; 28 | } 29 | 30 | after(...nodes: (string | Node)[]): void { 31 | throw new Error('The method "DocumentType.prototype.after()" not implemented.'); 32 | } 33 | before(...nodes: (string | Node)[]): void { 34 | throw new Error('The method "DocumentType.prototype.before()" not implemented.'); 35 | } 36 | remove(): void { 37 | throw new Error('The method "DocumentType.prototype.remove()" not implemented.'); 38 | } 39 | replaceWith(...nodes: (string | Node)[]): void { 40 | throw new Error('The method "DocumentType.prototype.replaceWith()" not implemented.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLAudioElement.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import HTMLMediaElementImpl from './HTMLMediaElement'; 3 | 4 | export default class HTMLAudioElementImpl extends HTMLMediaElementImpl implements HTMLAudioElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args: any[], 8 | _privateData = null 9 | ) { 10 | super(hostObject, args, { localName: 'audio' }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLBaseElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { fallbackBaseURL } from '../helpers/document-base-url'; 3 | import { HTMLElementImpl } from './HTMLElement'; 4 | 5 | export default class HTMLBaseElementImpl extends HTMLElementImpl implements HTMLBaseElement { 6 | constructor( 7 | nativeDocument: NativeDocument, 8 | args, 9 | privateData: {} = null 10 | ) { 11 | super(nativeDocument, args, { 12 | localName: 'base', 13 | }); 14 | } 15 | get href(): string { 16 | const document = this._ownerDocument; 17 | const url = this.hasAttributeNS(null, 'href') ? this.getAttributeNS(null, 'href') : ''; 18 | const parsed = new URL(url, fallbackBaseURL(document)); 19 | if (parsed === null) { 20 | return url; 21 | } 22 | return parsed.href; 23 | } 24 | set href(value: string) { 25 | this.setAttributeNS(null, 'href', value); 26 | } 27 | 28 | get target(): string { 29 | return this.getAttributeNS(null, 'target') || ''; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLDivElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { HTMLContentElement } from './HTMLContentElement'; 3 | 4 | export default class HTMLDivElementImpl extends HTMLContentElement implements HTMLDivElement { 5 | align: string; 6 | 7 | constructor( 8 | nativeDocument: NativeDocument, 9 | args, 10 | privateData: {} = null 11 | ) { 12 | super(nativeDocument, args, { 13 | localName: 'div', 14 | }); 15 | 16 | this._adoptedStyle.flexDirection = 'column'; 17 | this._adoptedStyle.width = '100%'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLHeadElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { HTMLElementImpl } from './HTMLElement'; 3 | 4 | export default class HTMLHeadElementImpl extends HTMLElementImpl implements HTMLHeadElement { 5 | constructor( 6 | nativeDocument: NativeDocument, 7 | args, 8 | privateData: {} = null 9 | ) { 10 | super(nativeDocument, args, { 11 | localName: 'head', 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLHeadingElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { HTMLContentElement } from './HTMLContentElement'; 3 | 4 | export default class HTMLHeadingElementImpl extends HTMLContentElement implements HTMLHeadingElement { 5 | align: string; 6 | 7 | constructor( 8 | nativeDocument: NativeDocument, 9 | args, 10 | privateData: { 11 | level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 12 | } 13 | ) { 14 | super(nativeDocument, args, { 15 | localName: privateData.level, 16 | }); 17 | this._adoptedStyle.display = 'block'; 18 | this._adoptedStyle.textAlign = 'left'; 19 | this._adoptedStyle.lineHeight = '1.5'; 20 | this._adoptedStyle.fontWeight = 'bold'; 21 | 22 | switch (privateData.level) { 23 | case 'h1': 24 | this._adoptedStyle.fontSize = '50%'; 25 | this._adoptedStyle.margin = '2% 0'; 26 | break; 27 | case 'h2': 28 | this._adoptedStyle.fontSize = '47%'; 29 | this._adoptedStyle.margin = '2% 0'; 30 | break; 31 | case 'h3': 32 | this._adoptedStyle.fontSize = '45%'; 33 | this._adoptedStyle.margin = '2% 0'; 34 | break; 35 | case 'h4': 36 | this._adoptedStyle.fontSize = '42%'; 37 | this._adoptedStyle.margin = '2% 0'; 38 | break; 39 | case 'h5': 40 | this._adoptedStyle.fontSize = '40%'; 41 | this._adoptedStyle.margin = '2% 0'; 42 | break; 43 | case 'h6': 44 | this._adoptedStyle.fontSize = '37%'; 45 | this._adoptedStyle.margin = '2% 0'; 46 | break; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLParagraphElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { HTMLContentElement } from './HTMLContentElement'; 3 | 4 | export default class HTMLParagraphElementImpl extends HTMLContentElement implements HTMLParagraphElement { 5 | align: string; 6 | 7 | constructor( 8 | nativeDocument: NativeDocument, 9 | args, 10 | _privateData: {} = null 11 | ) { 12 | super(nativeDocument, args, { 13 | localName: 'p', 14 | }); 15 | this._adoptedStyle.display = 'block'; 16 | this._adoptedStyle.textAlign = 'left'; 17 | this._adoptedStyle.lineHeight = '1.2'; 18 | this._adoptedStyle.margin = '1% 0'; 19 | this._adoptedStyle.fontSize = '30%'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLScriptElement.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import HTMLScriptElementImpl from './HTMLScriptElement'; 3 | 4 | describe('HTMLScriptElement.supports', () => { 5 | it('should return supports value', () => { 6 | expect(HTMLScriptElementImpl.supports('script')).toBe(true); 7 | expect(HTMLScriptElementImpl.supports('module')).toBe(true); 8 | expect(HTMLScriptElementImpl.supports('loader')).toBe(true); 9 | expect(HTMLScriptElementImpl.supports('importmap')).toBe(false); 10 | expect(HTMLScriptElementImpl.supports('speculationrules')).toBe(false); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLSpanElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { HTMLContentElement } from './HTMLContentElement'; 3 | 4 | export default class HTMLSpanElementImpl extends HTMLContentElement implements HTMLSpanElement { 5 | constructor( 6 | nativeDocument: NativeDocument, 7 | args, 8 | _privateData: {} = null 9 | ) { 10 | super(nativeDocument, args, { 11 | localName: 'span', 12 | }); 13 | 14 | this._adoptedStyle.flexDirection = 'row'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/living/nodes/HTMLTitleElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { childTextContent } from '../helpers/text'; 3 | import { HTMLElementImpl } from './HTMLElement'; 4 | 5 | export default class HTMLTitleElementImpl extends HTMLElementImpl implements HTMLTitleElement { 6 | constructor( 7 | nativeDocument: NativeDocument, 8 | args, 9 | privateData: {} = null 10 | ) { 11 | super(nativeDocument, args, { 12 | localName: 'title', 13 | }); 14 | } 15 | 16 | get text(): string { 17 | return childTextContent(this); 18 | } 19 | 20 | set text(value: string) { 21 | this.textContent = value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/living/nodes/NonDocumentTypeChildNode.ts: -------------------------------------------------------------------------------- 1 | import { domSymbolTree } from '../helpers/internal-constants'; 2 | import { NodeImpl } from './Node'; 3 | 4 | export default class NonDocumentTypeChildNodeImpl implements NonDocumentTypeChildNode { 5 | get nextElementSibling(): Element { 6 | for (const sibling of domSymbolTree.nextSiblingsIterator(this)) { 7 | if (sibling.nodeType === NodeImpl.ELEMENT_NODE) { 8 | return sibling; 9 | } 10 | } 11 | return null; 12 | } 13 | 14 | get previousElementSibling() { 15 | for (const sibling of domSymbolTree.previousSiblingsIterator(this)) { 16 | if (sibling.nodeType === NodeImpl.ELEMENT_NODE) { 17 | return sibling; 18 | } 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/living/nodes/NonElementParentNode.ts: -------------------------------------------------------------------------------- 1 | export default interface NonElementParentNodeImpl extends NonElementParentNode { } 2 | export default class NonElementParentNodeImpl implements NonElementParentNode { 3 | // Nothing 4 | } 5 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialBoundElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialBoundElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'bound', 12 | }); 13 | } 14 | 15 | _attach(): void { 16 | super._attach( 17 | new BABYLON.TransformNode(this._getInternalNodeNameOrId(), this._scene) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialCapsuleElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialCapsuleElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'capsule', 12 | }); 13 | } 14 | 15 | get height(): number { 16 | return parseFloat(this.getAttribute('height')); 17 | } 18 | set height(value: number) { 19 | this._setSpatialAttribute('height', value); 20 | } 21 | 22 | get radius(): number { 23 | return parseFloat(this.getAttribute('radius')); 24 | } 25 | set radius(value: number) { 26 | this._setSpatialAttribute('radius', value); 27 | } 28 | 29 | get radiusTop(): number { 30 | return parseFloat(this.getAttribute('radius-top')); 31 | } 32 | set radiusTop(value: number) { 33 | this._setSpatialAttribute('radius-top', value); 34 | } 35 | 36 | get radiusBottom(): number { 37 | return parseFloat(this.getAttribute('radius-bottom')); 38 | } 39 | set radiusBottom(value: number) { 40 | this._setSpatialAttribute('radius-bottom', value); 41 | } 42 | 43 | _attach(): void { 44 | super._attach( 45 | BABYLON.MeshBuilder.CreateCapsule(this._getInternalNodeNameOrId(), { 46 | ...this._getCommonMeshBuilderOptions(), 47 | height: this.height, 48 | radius: this.radius, 49 | radiusTop: this.radiusTop, 50 | radiusBottom: this.radiusBottom, 51 | }, this._scene) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialCubeElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialCubeElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'cube', 12 | }); 13 | } 14 | 15 | get size(): number { 16 | return parseFloat(this.getAttribute('size')); 17 | } 18 | set size(value: number) { 19 | this._setSpatialAttribute('size', value); 20 | } 21 | 22 | get width(): number { 23 | return parseFloat(this.getAttribute('width')); 24 | } 25 | set width(value: number) { 26 | this._setSpatialAttribute('width', value); 27 | } 28 | 29 | get height(): number { 30 | return parseFloat(this.getAttribute('height')); 31 | } 32 | set height(value: number) { 33 | this._setSpatialAttribute('height', value); 34 | } 35 | 36 | get depth(): number { 37 | return parseFloat(this.getAttribute('depth')); 38 | } 39 | set depth(value: number) { 40 | this._setSpatialAttribute('depth', value); 41 | } 42 | 43 | _attach(): void { 44 | super._attach( 45 | BABYLON.MeshBuilder.CreateBox(this._getInternalNodeNameOrId(), { 46 | ...this._getCommonMeshBuilderOptions(), 47 | size: this.size, 48 | width: this.width, 49 | depth: this.depth, 50 | height: this.height, 51 | }, this._scene) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialPlaneElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialPlaneElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'plane', 12 | }); 13 | } 14 | 15 | get size(): number { 16 | return parseFloat(this.getAttribute('size')); 17 | } 18 | set size(value: number) { 19 | this._setSpatialAttribute('size', value); 20 | } 21 | 22 | get width(): number { 23 | return parseFloat(this.getAttribute('width')) || 1; 24 | } 25 | set width(value: number) { 26 | this._setSpatialAttribute('width', value); 27 | } 28 | 29 | get height(): number { 30 | return parseFloat(this.getAttribute('height')) || 1; 31 | } 32 | set height(value: number) { 33 | this._setSpatialAttribute('height', value); 34 | } 35 | 36 | override get textureSizeRatio(): number { 37 | return this.height / this.width; 38 | } 39 | 40 | _attach(): void { 41 | super._attach( 42 | BABYLON.MeshBuilder.CreatePlane(this._getInternalNodeNameOrId(), { 43 | ...this._getCommonMeshBuilderOptions(), 44 | size: this.size, 45 | width: this.width, 46 | height: this.height, 47 | }, this._scene) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialRefElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialRefElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'ref', 12 | }); 13 | } 14 | 15 | ref(target: BABYLON.Node) { 16 | this._internalObject = target; 17 | this.setAttribute('id', target.id); 18 | this.setAttribute('name', target.name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialSpaceElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialSpaceElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'space', 12 | }); 13 | } 14 | 15 | _attach(): void { 16 | const spaceNode = new BABYLON.TransformNode(this._getInternalNodeNameOrId(), this._scene); 17 | spaceNode.setEnabled(false); // Disable the node by default. 18 | super._attach(spaceNode); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/living/nodes/SpatialTorusElement.ts: -------------------------------------------------------------------------------- 1 | import { NativeDocument } from '../../impl-interfaces'; 2 | import { SpatialElement } from './SpatialElement'; 3 | 4 | export default class SpatialTorusElement extends SpatialElement { 5 | constructor( 6 | hostObject: NativeDocument, 7 | args, 8 | _privateData: {} = null, 9 | ) { 10 | super(hostObject, args, { 11 | localName: 'torus', 12 | }); 13 | } 14 | 15 | get diameter(): number { 16 | return parseFloat(this.getAttribute('diameter')); 17 | } 18 | set diameter(value: number) { 19 | this._setSpatialAttribute('diameter', value); 20 | } 21 | 22 | get thickness(): number { 23 | return parseFloat(this.getAttribute('thickness')); 24 | } 25 | set thickness(value: number) { 26 | this._setSpatialAttribute('thickness', value); 27 | } 28 | 29 | get tessellation(): number { 30 | return parseFloat(this.getAttribute('tessellation')); 31 | } 32 | set tessellation(value: number) { 33 | this._setSpatialAttribute('tessellation', value); 34 | } 35 | 36 | _attach(): void { 37 | super._attach( 38 | BABYLON.MeshBuilder.CreateTorus(this._getInternalNodeNameOrId(), { 39 | ...this._getCommonMeshBuilderOptions(), 40 | diameter: this.diameter, 41 | thickness: this.thickness, 42 | tessellation: this.tessellation, 43 | }, this._scene) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/living/range/AbstractRange.ts: -------------------------------------------------------------------------------- 1 | export interface BoundaryPoint { 2 | node: Node; 3 | offset: number; 4 | } 5 | 6 | // https://dom.spec.whatwg.org/#abstractrange 7 | export class AbstractRangeImpl implements AbstractRange { 8 | _hostObject: any; 9 | _start: BoundaryPoint; 10 | _end: BoundaryPoint; 11 | 12 | constructor(hostObject, args, privateData) { 13 | const { start, end } = privateData; 14 | 15 | this._start = start; 16 | this._end = end; 17 | this._hostObject = hostObject; 18 | } 19 | 20 | // https://dom.spec.whatwg.org/#dom-range-startcontainer 21 | get startContainer() { 22 | return this._start.node; 23 | } 24 | 25 | // https://dom.spec.whatwg.org/#dom-range-startoffset 26 | get startOffset() { 27 | return this._start.offset; 28 | } 29 | 30 | // https://dom.spec.whatwg.org/#dom-range-endcontainer 31 | get endContainer() { 32 | return this._end.node; 33 | } 34 | 35 | // https://dom.spec.whatwg.org/#dom-range-endoffset 36 | get endOffset() { 37 | return this._end.offset; 38 | } 39 | 40 | // https://dom.spec.whatwg.org/#dom-range-collapsed 41 | get collapsed() { 42 | const { _start, _end } = this; 43 | return _start.node === _end.node && _start.offset === _end.offset; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/living/script-context.ts: -------------------------------------------------------------------------------- 1 | import type assert from 'assert'; 2 | import type buffer from 'buffer'; 3 | import type { BaseWindowImpl as BaseWindow } from '../agent/window'; 4 | import type { SpatialDocumentImpl as SpatialDocument } from './nodes/SpatialDocument'; 5 | import type NoiseImpl from './crypto/Noise'; 6 | 7 | /** 8 | * Represents the script context interface. 9 | */ 10 | export interface ScriptContext { 11 | BABYLON: typeof BABYLON; 12 | Buffer: typeof buffer.Buffer; 13 | assert: typeof assert; 14 | 15 | URL: typeof URL; 16 | Blob: typeof Blob; 17 | WebSocket: typeof WebSocket; 18 | Audio: typeof Audio; 19 | ImageData: typeof ImageData; 20 | OffscreenCanvas: typeof OffscreenCanvas; 21 | 22 | DOMPoint: typeof DOMPoint; 23 | DOMPointReadOnly: typeof DOMPointReadOnly; 24 | DOMRect: typeof DOMRect; 25 | DOMRectReadOnly: typeof DOMRectReadOnly; 26 | Noise: typeof NoiseImpl; 27 | 28 | atob: typeof atob; 29 | btoa: typeof btoa; 30 | fetch: BaseWindow['fetch']; 31 | setTimeout: typeof setTimeout; 32 | clearTimeout: typeof clearTimeout; 33 | setInterval: typeof setInterval; 34 | clearInterval: typeof clearInterval; 35 | 36 | getComputedStyle: typeof getComputedStyle; 37 | getComputedSpatialStyle: BaseWindow['getComputedSpatialStyle']; 38 | createImageBitmap: typeof createImageBitmap; 39 | 40 | console: Console; 41 | navigator: Navigator; 42 | document: SpatialDocument; 43 | spatialDocument: SpatialDocument; 44 | /** 45 | * @deprecated 46 | */ 47 | spaceDocument: SpatialDocument; 48 | } 49 | -------------------------------------------------------------------------------- /src/living/traversal/helpers.ts: -------------------------------------------------------------------------------- 1 | import { NodeImpl } from '../nodes/Node'; 2 | import NodeIteratorImpl from './NodeIterator'; 3 | 4 | export const FILTER_ACCEPT = 1; // NodeFilter.FILTER_ACCEPT 5 | export const FILTER_REJECT = 2; // NodeFilter.FILTER_REJECT 6 | export const FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP 7 | 8 | export function filter(nodeIteratorOrTreeWalkerImpl: NodeIteratorImpl, nodeImpl: NodeImpl) { 9 | if (nodeIteratorOrTreeWalkerImpl._active) { 10 | throw new DOMException( 11 | 'Recursive node filtering', 12 | 'InvalidStateError' 13 | ); 14 | } 15 | 16 | const n = nodeImpl.nodeType - 1; 17 | if (!((1 << n) & nodeIteratorOrTreeWalkerImpl.whatToShow)) { 18 | return exports.FILTER_SKIP; 19 | } 20 | 21 | // Saving in a variable is important so we don't accidentally call it as a method later. 22 | const { filter } = nodeIteratorOrTreeWalkerImpl; 23 | if (filter === null) { 24 | return exports.FILTER_ACCEPT; 25 | } 26 | 27 | nodeIteratorOrTreeWalkerImpl._active = true; 28 | 29 | let result: number; 30 | // https://github.com/whatwg/dom/issues/494 31 | try { 32 | if (typeof filter === 'function') { 33 | result = filter(nodeImpl); 34 | } else if (typeof filter.acceptNode === 'function') { 35 | result = filter.acceptNode(nodeImpl); 36 | } 37 | } finally { 38 | nodeIteratorOrTreeWalkerImpl._active = false; 39 | } 40 | return result; 41 | } 42 | -------------------------------------------------------------------------------- /src/living/xr/XRInputSource.ts: -------------------------------------------------------------------------------- 1 | class XRInputSourceImpl implements XRInputSource { 2 | handedness: XRHandedness; 3 | targetRayMode: XRTargetRayMode; 4 | targetRaySpace: XRSpace; 5 | gripSpace?: XRSpace; 6 | gamepad?: Gamepad; 7 | profiles: string[]; 8 | hand?: XRHand; 9 | } 10 | -------------------------------------------------------------------------------- /src/living/xr/XRInputSourceArray.ts: -------------------------------------------------------------------------------- 1 | export default class XRInputSourceArrayImpl extends Array implements XRInputSourceArray { 2 | static createFromIterator(sourcesIterator: IterableIterator): XRInputSourceArrayImpl { 3 | const arr = new XRInputSourceArrayImpl(); 4 | for (const source of sourcesIterator) { 5 | arr.push(source); 6 | } 7 | return arr; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/living/xr/XRJointSpace.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import { kCreateForImpl } from '../../symbols'; 3 | import XRPoseImpl from './XRPose'; 4 | import XRRigidTransformImpl from './XRRigidTransform'; 5 | import XRSpaceImpl, { kSpacePose } from './XRSpace'; 6 | 7 | type PoseInit = { 8 | position: DOMPointInit; 9 | rotation: DOMPointInit; 10 | }; 11 | 12 | export default class XRJointSpaceImpl extends XRSpaceImpl implements XRJointSpace { 13 | jointName: XRHandJoint; 14 | 15 | static [kCreateForImpl]( 16 | hostObject: NativeDocument, 17 | jointName: XRHandJoint, 18 | jointPose: PoseInit 19 | ): XRJointSpaceImpl { 20 | const impl = new XRJointSpaceImpl(hostObject, []); 21 | impl.jointName = jointName; 22 | impl[kSpacePose] = XRPoseImpl.createForImpl(impl._hostObject, [], { 23 | transform: new XRRigidTransformImpl(jointPose.position, jointPose.rotation), 24 | emulatedPosition: false, 25 | // TODO: support linearVelocity and angularVelocity? 26 | }); 27 | return impl; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/living/xr/XRPose.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | 3 | export default class XRPoseImpl implements XRPose { 4 | _hostObject: NativeDocument; 5 | _transform: XRRigidTransform; 6 | _emulatedPosition: boolean; 7 | _linearVelocity?: DOMPointReadOnly; 8 | _angularVelocity?: DOMPointReadOnly; 9 | 10 | get transform(): XRRigidTransform { 11 | return this._transform; 12 | } 13 | 14 | get emulatedPosition(): boolean { 15 | return this._emulatedPosition; 16 | } 17 | 18 | get linearVelocity(): DOMPointReadOnly | undefined { 19 | return this._linearVelocity; 20 | } 21 | 22 | get angularVelocity(): DOMPointReadOnly | undefined { 23 | return this._angularVelocity; 24 | } 25 | 26 | static createForImpl( 27 | hostObject: NativeDocument, 28 | _args: any[], 29 | privateData: { 30 | transform: XRRigidTransform, 31 | emulatedPosition: boolean, 32 | linearVelocity?: DOMPointReadOnly, 33 | angularVelocity?: DOMPointReadOnly, 34 | } 35 | ): XRPoseImpl { 36 | const pose = new XRPoseImpl(); 37 | pose._hostObject = hostObject; 38 | pose._transform = privateData.transform; 39 | pose._emulatedPosition = privateData.emulatedPosition; 40 | pose._linearVelocity = privateData.linearVelocity; 41 | pose._angularVelocity = privateData.angularVelocity; 42 | return pose; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/living/xr/XRRigidTransform.ts: -------------------------------------------------------------------------------- 1 | import DOMExceptionImpl from '../domexception'; 2 | import DOMPointReadOnlyImpl from '../geometry/DOMPointReadOnly'; 3 | 4 | export default class XRRigidTransformImpl implements XRRigidTransform { 5 | _position: DOMPointReadOnlyImpl = new DOMPointReadOnlyImpl(0, 0, 0, 1); 6 | _orientation: DOMPointReadOnlyImpl = new DOMPointReadOnlyImpl(0, 0, 0, 1); 7 | _matrix: Float32Array = new Float32Array(16); 8 | 9 | get position(): DOMPointReadOnly { 10 | return this._position; 11 | } 12 | 13 | get orientation(): DOMPointReadOnly { 14 | return this._orientation; 15 | } 16 | 17 | get matrix(): Float32Array { 18 | return this._matrix; 19 | } 20 | 21 | get inverse(): XRRigidTransform { 22 | throw new DOMExceptionImpl('Method not implemented.', 'NOT_SUPPORTED_ERR'); 23 | } 24 | 25 | constructor(position?: DOMPointInit, orientation?: DOMPointInit) { 26 | if (position) { 27 | this._position = new DOMPointReadOnlyImpl(position.x, position.y, position.z, position.w); 28 | } 29 | if (orientation) { 30 | this._orientation = new DOMPointReadOnlyImpl(orientation.x, orientation.y, orientation.z, orientation.w); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/living/xr/XRSpace.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import XRPoseImpl from './XRPose'; 3 | 4 | export const kSpacePose = Symbol('kSpacePose'); 5 | 6 | export default class XRSpaceImpl extends EventTarget implements XRSpace { 7 | [kSpacePose]: XRPoseImpl; 8 | 9 | constructor( 10 | protected _hostObject: NativeDocument, 11 | _args: any[], 12 | _privateData = null 13 | ) { 14 | super(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/living/xr/XRSystem.ts: -------------------------------------------------------------------------------- 1 | import type { NativeDocument } from '../../impl-interfaces'; 2 | import XRSessionImpl from './XRSession'; 3 | 4 | export const kSession = Symbol('kSession'); 5 | 6 | export default class XRSystemImpl extends EventTarget implements XRSystem { 7 | #nativeDocument: NativeDocument; 8 | [kSession]: XRSessionImpl | null = null; 9 | 10 | ondevicechange: XRSystemDeviceChangeEventHandler; 11 | onsessiongranted: XRSystemSessionGrantedEventHandler; 12 | 13 | constructor( 14 | hostObject: NativeDocument, 15 | _args: any[], 16 | _privateData = null 17 | ) { 18 | super(); 19 | this.#nativeDocument = hostObject; 20 | } 21 | 22 | async requestSession(mode: XRSessionMode, options?: XRSessionInit): Promise { 23 | this[kSession] = await XRSessionImpl.createForImpl(this.#nativeDocument, [mode, options]); 24 | return this[kSession]; 25 | } 26 | 27 | async isSessionSupported(mode: XRSessionMode): Promise { 28 | const hostObject = this.#nativeDocument; 29 | if (typeof hostObject.userAgent.isXRSessionSupported !== 'function') { 30 | return false; 31 | } else { 32 | return hostObject.userAgent.isXRSessionSupported(mode); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/mixin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Applies mixins to a derived class. 3 | * 4 | * @param derivedCtor - The derived class constructor. 5 | * @param constructors - An array of mixin constructors. 6 | */ 7 | export function applyMixins(derivedCtor: any, constructors: any[]) { 8 | constructors.forEach((baseCtor) => { 9 | Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { 10 | // Skip the name "constructor" 11 | if (name === 'constructor') { 12 | return; 13 | } 14 | Object.defineProperty( 15 | derivedCtor.prototype, 16 | name, 17 | Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || 18 | Object.create(null) 19 | ); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | export const DIRTY_SYMBOL = Symbol('__dirty__'); 2 | export const MIME_TYPE_SYMBOL = Symbol('__mimeType__'); 3 | 4 | /** 5 | * The Symbol used to store the guid in the transmute protocol, which is such important because it's used to 6 | * identify a game object in the running mode. 7 | */ 8 | export const SPATIAL_OBJECT_GUID_SYMBOL = Symbol('__spatialObjectGuid__'); 9 | 10 | /** 11 | * The Symbol used to store the craft3d node, which is used for layout algorithms in space. 12 | */ 13 | export const SPATIAL_ELEMENT_CRAFT3D_NODE_SYMBOL = Symbol('__spatialElementCraft3dNode__'); 14 | 15 | /** 16 | * The Symbol used to create an instance of a class in Web APIs. 17 | */ 18 | export const kCreateForImpl = Symbol('createForImpl'); 19 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, jest, afterEach } from '@jest/globals'; 2 | import { treeOrderSorter } from './utils'; 3 | import { domSymbolTree } from './living/helpers/internal-constants'; 4 | import symbolTree from 'symbol-tree'; 5 | 6 | describe('treeOrderSorter', () => { 7 | let compareTreePosition: ReturnType; 8 | afterEach(() => { 9 | compareTreePosition.mockRestore(); 10 | }); 11 | 12 | it('should return 1 if b is preceding a', () => { 13 | // Arrange 14 | const a = {}; 15 | const b = {}; 16 | compareTreePosition = jest.spyOn(domSymbolTree, 'compareTreePosition') 17 | .mockReturnValue(symbolTree.TreePosition.PRECEDING); 18 | 19 | // Act 20 | const result = treeOrderSorter(a, b); 21 | 22 | // Assert 23 | expect(result).toBe(1); 24 | expect(compareTreePosition).toHaveBeenCalledWith(a, b); 25 | }); 26 | 27 | it('should return -1 if b is following a', () => { 28 | // Arrange 29 | const a = {}; 30 | const b = {}; 31 | compareTreePosition = jest.spyOn(domSymbolTree, 'compareTreePosition') 32 | .mockReturnValue(symbolTree.TreePosition.FOLLOWING); 33 | 34 | // Act 35 | const result = treeOrderSorter(a, b); 36 | 37 | // Assert 38 | expect(result).toBe(-1); 39 | expect(compareTreePosition).toHaveBeenCalledWith(a, b); 40 | }); 41 | 42 | it('should return 0 if a and b are disconnected or equal', () => { 43 | // Arrange 44 | const a = {}; 45 | const b = {}; 46 | compareTreePosition = jest.spyOn(domSymbolTree, 'compareTreePosition') 47 | .mockReturnValue(0); 48 | 49 | // Act 50 | const result = treeOrderSorter(a, b); 51 | 52 | // Assert 53 | expect(result).toBe(0); 54 | expect(compareTreePosition).toHaveBeenCalledWith(a, b); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/virtual-console.ts: -------------------------------------------------------------------------------- 1 | export class VirtualConsole extends EventTarget { 2 | constructor() { 3 | super(); 4 | 5 | this.addEventListener('error', () => {}); 6 | } 7 | 8 | sendTo(anyConsole, options) { 9 | // TODO 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig-esm-loader.js: -------------------------------------------------------------------------------- 1 | import { resolve as resolveTs } from 'ts-node/esm'; 2 | import * as tsConfigPaths from 'tsconfig-paths'; 3 | import { pathToFileURL } from 'url'; 4 | 5 | const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig(); 6 | const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths); 7 | 8 | export function resolve(specifier, ctx, defaultResolve) { 9 | const match = matchPath(specifier); 10 | return match 11 | ? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve) 12 | : resolveTs(specifier, ctx, defaultResolve); 13 | } 14 | 15 | export { load, transformSource } from 'ts-node/esm'; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "ESNext", 5 | "outDir": "dist", 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true, 9 | "allowJs": true, 10 | "types": [ 11 | "node" 12 | ], 13 | "lib": [ 14 | "ESNext.WeakRef", 15 | "ESNext", 16 | "DOM" 17 | ], 18 | "baseUrl": "./", 19 | "paths": { 20 | "@bindings/taffy": ["./bindings/taffy"], 21 | "@bindings/noise": ["./bindings/noise"], 22 | } 23 | }, 24 | "include": [ 25 | "src/**/*.ts" 26 | ], 27 | "exclude": [ 28 | "types", 29 | "fixtures", 30 | "node_modules" 31 | ], 32 | "ts-node": { 33 | "esm": true, 34 | "moduleTypes": { 35 | "**/*.ts": "esm", 36 | "**/*.js": "esm", 37 | "**/*.mjs": "esm", 38 | "**/*.cjs": "cjs" 39 | }, 40 | "experimentalSpecifierResolution": "node" 41 | } 42 | } -------------------------------------------------------------------------------- /types/.gitignore: -------------------------------------------------------------------------------- 1 | package.json 2 | *.d.ts 3 | !index.d.ts 4 | -------------------------------------------------------------------------------- /types/.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .package.json 3 | 4 | node_modules 5 | build.cjs 6 | package-lock.json 7 | tsconfig.json 8 | -------------------------------------------------------------------------------- /types/.package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yodaos-jsar/types", 3 | "version": "{{version}}", 4 | "description": "The TypeScript definitions for JSAR application developers", 5 | "types": "index.d.ts", 6 | "author": "Yazhong Liu ", 7 | "license": "Apache-2.0", 8 | "typeScriptVersion": "4.3", 9 | "dependencies": { 10 | "@types/node": "^18.12.1", 11 | "babylonjs": "^6.10.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/build.cjs: -------------------------------------------------------------------------------- 1 | const fsPromises = require('fs/promises'); 2 | const supports = require('../src/living/esm-supports.json'); 3 | const { join } = require('path'); 4 | 5 | function generateLoaderBlock(ext, type) { 6 | return [ 7 | `declare module \'*${ext}\' {`, 8 | ` const value: ${type};`, 9 | ' export default value;', 10 | '}', 11 | ].join('\n') + '\n'; 12 | } 13 | 14 | function copyApiDts() { 15 | fsPromises.copyFile( 16 | join(__dirname, '../dist/jsar-api.d.ts'), 17 | join(__dirname, './jsar-api.d.ts') 18 | ); 19 | } 20 | 21 | function createLoadersDts() { 22 | let dtsCode = ''; 23 | for (const extension of supports.extensions.json) { 24 | dtsCode += generateLoaderBlock(extension, 'any'); 25 | } 26 | for (const extension of supports.extensions.arraybuffer) { 27 | dtsCode += generateLoaderBlock(extension, 'ArrayBuffer'); 28 | } 29 | fsPromises.writeFile(join(__dirname, 'loaders.d.ts'), dtsCode); 30 | } 31 | 32 | function createPackageJson() { 33 | const packageJson = require('./.package.json'); 34 | const { version } = require('../package.json'); 35 | packageJson.version = version; 36 | fsPromises.writeFile( 37 | join(__dirname, 'package.json'), 38 | JSON.stringify(packageJson, null, 2)); 39 | } 40 | 41 | copyApiDts(); 42 | createLoadersDts(); 43 | createPackageJson(); 44 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | 7 | import { ScriptContext } from '@yodaos-jsar/dom'; 8 | 9 | declare global { 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 11 | // @ts-ignore 12 | const document: ScriptContext['document']; 13 | const spatialDocument: ScriptContext['spatialDocument']; 14 | const spaceDocument: ScriptContext['spaceDocument']; 15 | const getComputedSpatialStyle: ScriptContext['getComputedSpatialStyle']; 16 | } 17 | -------------------------------------------------------------------------------- /types/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yodaos-jsar/types", 3 | "version": "{{version}}", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@yodaos-jsar/types", 9 | "version": "{{version}}", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@types/node": "^18.12.1", 13 | "babylonjs": "^6.10.0" 14 | } 15 | }, 16 | "node_modules/@types/node": { 17 | "version": "18.19.3", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", 19 | "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", 20 | "dependencies": { 21 | "undici-types": "~5.26.4" 22 | } 23 | }, 24 | "node_modules/babylonjs": { 25 | "version": "6.33.2", 26 | "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-6.33.2.tgz", 27 | "integrity": "sha512-cIZ1lEFPlgeZ7ziL3fsb5kaAQuwt+ZwqOMk3XztfRPZ1WaNt+MR8HmyvUMnKR2kKY9Uc2gduj3rhUMDq3n7xcA==", 28 | "hasInstallScript": true 29 | }, 30 | "node_modules/undici-types": { 31 | "version": "5.26.5", 32 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 33 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "types": [], 6 | "lib": [ 7 | "ESNext" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ], 13 | "include": [ 14 | "*.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const packageJson = require('./package.json'); 3 | 4 | function getFormattedDate(timestamp) { 5 | const date = new Date(timestamp); 6 | const year = date.getFullYear(); 7 | const month = (date.getMonth() + 1).toString().padStart(2, '0'); 8 | const day = date.getDate().toString().padStart(2, '0'); 9 | 10 | return `${year}${month}${day}`; 11 | } 12 | 13 | let { version: baseVersion } = packageJson; 14 | baseVersion = baseVersion.split('-')[0]; 15 | 16 | const nowTimestamp = Date.now(); 17 | const newVersion = `${baseVersion}-alpha.${getFormattedDate(nowTimestamp)}.${nowTimestamp}`; 18 | fs.writeFileSync('./package.json', JSON.stringify({ 19 | ...packageJson, 20 | version: newVersion, 21 | }, null, 2)); 22 | 23 | console.log( 24 | `package.json is updated`, fs.readFileSync('./package.json', 'utf-8')); 25 | --------------------------------------------------------------------------------