Przeglądaj źródła

Merge branch 'main' into next

alvarosabu 1 miesiąc temu
rodzic
commit
0731c19c07

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -54,7 +54,7 @@ body:
       options:
         - label: I agree to follow this project's [Code of Conduct](https://github.com/Tresjs/tres/blob/main/CODE_OF_CONDUCT.md)
           required: true
-        - label: Read the [Contributing Guidelines](https://github.com/Tresjs/tres/blob/main/CONTRIBUTING.md).
+        - label: Read the [Contributing Guidelines](https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md).
           required: true
         - label: Read the [docs](https://tresjs.org/guide).
           required: true

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yml

@@ -39,7 +39,7 @@ body:
       options:
         - label: I agree to follow this project's [Code of Conduct](https://github.com/Tresjs/tres/blob/main/CODE_OF_CONDUCT.md)
           required: true
-        - label: Read the [Contributing Guidelines](https://github.com/Tresjs/tres/blob/main/CONTRIBUTING.md).
+        - label: Read the [Contributing Guidelines](https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md).
           required: true
         - label: Read the [docs](https://tresjs.org/guide).
           required: true

+ 11 - 0
CHANGELOG.md

@@ -70,6 +70,17 @@
 ### Bug Fixes
 
 * export logger utility from utils in index.ts ([#966](https://github.com/Tresjs/tres/issues/966)) ([bb0b9e2](https://github.com/Tresjs/tres/commit/bb0b9e2f3843d2bd27cd46cfe982f433dca013b4))
+## [4.3.5](https://github.com/Tresjs/tres/compare/4.3.4...4.3.5) (2025-05-16)
+
+### Bug Fixes
+
+* make sure key is camelCase when reached else on conditional ([#1011](https://github.com/Tresjs/tres/issues/1011)) ([2902d05](https://github.com/Tresjs/tres/commit/2902d05c600b0e2b02738d0ab5af292b4d75cc35))
+
+## [4.3.4](https://github.com/Tresjs/tres/compare/4.3.3...4.3.4) (2025-05-13)
+
+### Bug Fixes
+
+* **patchProp:** harden props inference ([#1006](https://github.com/Tresjs/tres/issues/1006)) ([6cdf28b](https://github.com/Tresjs/tres/commit/6cdf28b73a246b935f0b58a8b759a0aa1b925ff6))
 * revert improve type safety in retargeting proxy setter ([#930](https://github.com/Tresjs/tres/issues/930)) ([0a95764](https://github.com/Tresjs/tres/commit/0a95764ac47b93d58fd0668327658fe4aae53783))
 
 ### Reverts

+ 0 - 98
CONTRIBUTING.md

@@ -1,98 +0,0 @@
-![repository-banner.png](https://res.cloudinary.com/alvarosaburido/image/upload/v1683452574/repo-banner_d2xeem.png)
-
-# Tres Contributing Guide
-
-Hi there!! If you are reading this guide, you are probably interested in contributing to Tres. You're awesome 🤩.
-
-No contribution is too small, whether it is a typo in the docs, a bug report or refactoring a piece of code, every contribution is welcome, just make sure to follow the guidelines below ✌️.
-
-Thanks from the heart 💚 for taking the time to help out. This guide will help you to get started with the project.
-
-## Ecosystem
-- [@tresjs/core](https://github.com/Tresjs/tres) - The core package.
-- [@tresjs/cientos](https://github.com/Tresjs/cientos) - The abstractions package.
-- [@tresjs/postprocessing](https://github.com/Tresjs/post-processing) - The post-processing package.
-
-## Setup
-
-All the packages in the ecosystem use [pnpm workspaces](https://pnpm.io/workspaces). Pnpm is a package manager that is faster than npm and yarn. It also uses symlinks to avoid code duplication.
-
-The `workspace` has the following structure:
-
-```
-.
-├── docs // The documentation
-├── playground // The playground to test the package
-├── src // The source code
-
-```
-
-Make sure you are using [Node.js](https://nodejs.org/en/) version 14 or higher.
-
-You can install pnpm using npm:
-
-```bash
-npm install -g pnpm
-```
-
-or using homebrew:
-
-If you have the package manager installed, you can install pnpm using the following command:
-
-```
-brew install pnpm
-```
-
-## Development
-
-To start developing, you can run `pnpm run playground` in the root folder.
-
-This will start the dev server for the playground at `http://localhost:5173/` where you can test the changes you are making in the `src` folder.
-
-> **Important**
-> There is no need to run anything in the `src` folder or in the root, the `playground` will take care of it
-
-Whenever you are working on a new feature or fixing a bug, make sure to add a demo under `playground/src/pages` and create a route in the `playground/src/router.ts` to test the changes you are making.
-
-> **Warning**
-> Make sure to check if there is already a demo for the feature you are working on. If so, feel free to add your changes to the existing demo.
-
-### Docs
-
-The docs are built using [vitepress](https://vitepress.vuejs.org/).
-
-You can run `pnpm docs:dev` to start the dev server for the documentation. All the docs are located in the `docs` folder in markdown.
-
-If you are adding a new page, make sure to add it to the `docs/.vitepress/config.ts` file following the sidebar structure.
-
-### Testing
-
-Currently there are no tests in place, but we are working on it. If you want to contribute with tests, please open an issue first to discuss the best approach.
-
-## Pull Requests
-
-Before opening a pull request, make sure to run `pnpm lint` to make sure the code is following the code style.
-
-- Checkout a topic branch from the base branch `main` branch and merge back against that branch.
-- Please follow the [commit message conventions](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) when committing your changes. This is important because the release notes will be automatically generated from these messages. Small scoped commits are always preferred, as it is easier to review them.
-- If adding new feature:
-  - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. We would reject feature PRs that are not first opened as suggestions except for trivial changes.
-  - Create a `feature/{issue-number}-add-test-to-core` branch for this feature. Make the name meaningful.
-  - PR title must start with `feat(pkg): Descriptive title`. For example: `feat(core): added unit test to composables`.
-- If fixing a bug 🐛:
-
-  - Provide detailed description of the bug in the PR. Live demo preferred.
-  - Create a `fix/{issue-number}-fix-test-in-core` branch for this bug fix.
-  - If you are resolving a special issue, add `(fix #xxx[,#xxx])` (#xxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`.
-
-## Third Party Libraries
-
-Adding a new third party library is generally discouraged, unless it is absolutely necessary. If you want to add a new library, please open an issue first to discuss the best approach.
-
-## Keep core small
-
-The core package should be as small as possible, it should only contain the core functionality of the library. If you are adding a new feature, please consider adding it as a plugin instead. for example, if you want to add support for [Effect Composer](https://threejs.org/examples/?q=compo#webgl_postprocessing_effectcomposer) you should create a new package called `@tresjs/post-processing` and add it as a plugin. If it's a smaller scope you can always add it to the `cientos` package.
-
-### Assets
-
-If you need/want to add assets like models, videos, musics, textures, etc. Please consider adding to our [official assets repo](https://github.com/Tresjs/assets).

+ 4 - 0
docs/.vitepress/config/de.ts

@@ -123,6 +123,10 @@ export const deConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             text: 'Probleme',
             link: 'https://github.com/Tresjs/tres/issues',
           },
+          {
+            text: 'Beitragen',
+            link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+          },
           {
             text: 'Ökosystem',
             items: [

+ 10 - 0
docs/.vitepress/config/en.ts

@@ -111,6 +111,12 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           },
         ],
       },
+      {
+        text: 'Contributing',
+        items: [
+          { text: 'Contribute', link: '/contribute/contributing' },
+        ],
+      },
     ],
     nav: [
       { text: 'Guide', link: '/guide/' },
@@ -134,6 +140,10 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             text: 'Issues',
             link: 'https://github.com/Tresjs/tres/issues',
           },
+          {
+            text: 'Contributing',
+            link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+          },
           {
             text: 'Ecosystem',
             items: [

+ 4 - 0
docs/.vitepress/config/es.ts

@@ -124,6 +124,10 @@ export const esConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             text: 'Problemas',
             link: 'https://github.com/Tresjs/tres/issues',
           },
+          {
+            text: 'Contribuir',
+            link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+          },
           {
             text: 'Ecosistema',
             items: [

+ 4 - 0
docs/.vitepress/config/fr.ts

@@ -122,6 +122,10 @@ export const frConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           text: 'Problèmes',
           link: 'https://github.com/Tresjs/tres/issues',
         },
+        {
+          text: 'Contribuer',
+          link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+        },
         {
           text: 'Ecosystème',
           items: [

+ 4 - 0
docs/.vitepress/config/nl.ts

@@ -121,6 +121,10 @@ export const nlConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           text: 'Problemen',
           link: 'https://github.com/Tresjs/tres/issues',
         },
+        {
+          text: 'Bijdragen',
+          link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+        },
         {
           text: 'Ecosysteem',
           items: [

+ 4 - 0
docs/.vitepress/config/zh.ts

@@ -122,6 +122,10 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             text: '议题',
             link: 'https://github.com/Tresjs/tres/issues',
           },
+          {
+            text: '贡献',
+            link: 'https://github.com/Tresjs/.github/blob/main/CONTRIBUTING.md',
+          },
           {
             text: '生态系统',
             items: [

+ 13 - 0
docs/contribute/contributing.md

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+import { data } from './contribution.data.js'
+
+import { onMounted, ref } from 'vue'
+
+const content = ref('')
+
+onMounted(async () => {
+  content.value = data
+})
+</script>
+
+<div v-html="content" class="remote-markdown"></div>

+ 13 - 0
docs/contribute/contribution.data.js

@@ -0,0 +1,13 @@
+import { createMarkdownRenderer } from 'vitepress'
+
+export default {
+
+  async load() {
+    const config = globalThis.VITEPRESS_CONFIG
+    const md = await createMarkdownRenderer(config.srcDir, config.markdown, config.site.base, config.logger)
+    // fetch remote data
+    const response = await fetch('https://raw.githubusercontent.com/Tresjs/.github/main/CONTRIBUTING.md')
+    const content = await response.text()
+    return md.render(content)
+  },
+}

+ 1 - 0
playground/vue/components.d.ts

@@ -12,6 +12,7 @@ declare module 'vue' {
     BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
     DynamicModel: typeof import('./src/components/DynamicModel.vue')['default']
     GraphPane: typeof import('./src/components/GraphPane.vue')['default']
+    LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']
     OverlayInfo: typeof import('./src/components/OverlayInfo.vue')['default']
     PbrSphere: typeof import('./src/components/PbrSphere.vue')['default']
     ProvideBridge: typeof import('./src/components/ProvideBridge.vue')['default']

+ 375 - 0
playground/vue/src/components/LocalOrbitControls.vue

@@ -0,0 +1,375 @@
+<script lang="ts" setup>
+import { useLoop, useTresContext } from '@tresjs/core'
+import { useEventListener } from '@vueuse/core'
+import { MOUSE, TOUCH } from 'three'
+import { OrbitControls } from 'three-stdlib'
+import { onUnmounted, shallowRef, toRefs, watch } from 'vue'
+import type { TresVector3 } from '@tresjs/core'
+import type { Camera } from 'three'
+
+export interface OrbitControlsProps {
+  /**
+   * Whether to make this the default controls.
+   *
+   * @default false
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   */
+  makeDefault?: boolean
+  /**
+   * The camera to control.
+   *
+   * @type {Camera}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.camera
+   */
+  camera?: Camera
+  /**
+   * The dom element to listen to.
+   *
+   * @type {HTMLElement}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.domElement
+   */
+  domElement?: HTMLElement
+  /**
+   * The target to orbit around.
+   *
+   * @type {TresVector3}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.target
+   */
+  target?: TresVector3
+  /**
+   * Whether to enable damping (inertia)
+   *
+   * @default false
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableDamping
+   */
+  enableDamping?: boolean
+  /**
+   * The damping inertia used if `.enableDamping` is set to true
+   *
+   * @default 0.05
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.dampingFactor
+   */
+  dampingFactor?: number
+  /**
+   * Set to true to automatically rotate around the target.
+   *
+   * @default false
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.autoRotate
+   */
+  autoRotate?: boolean
+  /**
+   * How fast to rotate around the target if `.autoRotate` is true.
+   *
+   * @default 2
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.autoRotateSpeed
+   */
+  autoRotateSpeed?: number
+  /**
+   * Whether to enable panning.
+   *
+   * @default true
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enablePan
+   */
+  enablePan?: boolean
+  /**
+   * How fast to pan the camera when the keyboard is used. Default is 7.0 pixels per keypress.
+   *
+   * @default 7.0
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.keyPanSpeed
+   */
+  keyPanSpeed?: number
+  /**
+   * This object contains references to the keycodes for controlling camera panning.
+   * Default is the 4 arrow keys.
+   *
+   * @default `{ LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }`
+   * @type Record<string, string>
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.keys
+   */
+  keys?: Record<string, string>
+  /**
+   * How far you can orbit horizontally, upper limit.
+   * If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ],
+   * with ( max - min < 2 PI ). Default is Infinity.
+   *
+   * @default Infinity
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxAzimuthAngle
+   */
+  maxAzimuthAngle?: number
+  /**
+   * How far you can orbit horizontally, lower limit.
+   * If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ],
+   * with ( max - min < 2 PI ).
+   * Default is - Infinity.
+   *
+   * @default -Infinity
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minAzimuthAngle
+   */
+  minAzimuthAngle?: number
+  /**
+   * How far you can orbit vertically, upper limit.
+   * Range is 0 to Math.PI radians, and default is Math.PI.
+   *
+   * @default Math.PI
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxPolarAngle
+   */
+  maxPolarAngle?: number
+  /**
+   * How far you can orbit vertically, lower limit.
+   * Range is 0 to Math.PI radians, and default is 0.
+   *
+   * @default 0
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minPolarAngle
+   */
+  minPolarAngle?: number
+  /**
+   * The minimum distance of the camera to the target.
+   * Default is 0.
+   *
+   * @default 0
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minDistance
+   */
+  minDistance?: number
+  /**
+   * The maximum distance of the camera to the target.
+   * Default is Infinity.
+   *
+   * @default Infinity
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxDistance
+   */
+  maxDistance?: number
+  /**
+   * The minimum field of view angle, in radians.
+   * Default is 0.
+   *
+   * @default 0
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.minZoom
+   */
+  minZoom?: number
+  /**
+   * The maximum field of view angle, in radians.
+   * ( OrthographicCamera only ).
+   * Default is Infinity.
+   *
+   * @default Infinity
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/index.html?q=orbi#examples/en/controls/OrbitControls.maxZoom
+   */
+  maxZoom?: number
+  touches?: {
+    ONE?: number | undefined
+    TWO?: number | undefined
+  }
+  /**
+   * Whether to enable zooming.
+   *
+   * @default true
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableZoom
+   */
+  enableZoom?: boolean
+  /**
+   * How fast to zoom in and out. Default is 1.
+   *
+   * @default 1
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.zoomSpeed
+   */
+  zoomSpeed?: number
+  /**
+   * Whether to enable rotating.
+   *
+   * @default true
+   * @type {boolean}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.enableRotate
+   */
+  enableRotate?: boolean
+  /**
+   * How fast to rotate around the target. Default is 1.
+   *
+   * @default 1
+   * @type {number}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.rotateSpeed
+   */
+  rotateSpeed?: number
+  /**
+   * This object contains references to the mouse actions used by the controls.
+   * LEFT: Rotate around the target
+   * MIDDLE: Zoom the camera
+   * RIGHT: Pan the camera
+   *
+   * @default { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }
+   * @type {{ LEFT?: number, MIDDLE?: number, RIGHT?: number }}
+   * @memberof OrbitControlsProps
+   * @see https://threejs.org/docs/#examples/en/controls/OrbitControls.mouseButtons
+   */
+  mouseButtons?: {
+    LEFT?: number
+    MIDDLE?: number
+    RIGHT?: number
+  }
+}
+
+const props = withDefaults(defineProps<OrbitControlsProps>(), {
+  makeDefault: false,
+  autoRotate: false,
+  autoRotateSpeed: 2,
+  enableDamping: true,
+  dampingFactor: 0.05,
+  enablePan: true,
+  keyPanSpeed: 7,
+  maxAzimuthAngle: Number.POSITIVE_INFINITY,
+  minAzimuthAngle: Number.NEGATIVE_INFINITY,
+  maxPolarAngle: Math.PI,
+  minPolarAngle: 0,
+  minDistance: 0,
+  maxDistance: Number.POSITIVE_INFINITY,
+  minZoom: 0,
+  maxZoom: Number.POSITIVE_INFINITY,
+  enableZoom: true,
+  zoomSpeed: 1,
+  enableRotate: true,
+  touches: () => ({ ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }),
+  rotateSpeed: 1,
+  target: () => [0, 0, 0],
+  mouseButtons: () => ({ LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }),
+})
+
+const emit = defineEmits(['change', 'start', 'end'])
+
+const {
+  makeDefault,
+  autoRotate,
+  autoRotateSpeed,
+  enableDamping,
+  dampingFactor,
+  enablePan,
+  keyPanSpeed,
+  maxAzimuthAngle,
+  minAzimuthAngle,
+  maxPolarAngle,
+  minPolarAngle,
+  minDistance,
+  maxDistance,
+  minZoom,
+  maxZoom,
+  enableZoom,
+  zoomSpeed,
+  enableRotate,
+  touches,
+  rotateSpeed,
+  target,
+  mouseButtons,
+} = toRefs(props)
+
+const { camera: activeCamera, renderer, extend, controls, invalidate } = useTresContext()
+
+const controlsRef = shallowRef<OrbitControls | null>(null)
+
+extend({ OrbitControls })
+
+watch(controlsRef, (value) => {
+  addEventListeners()
+  if (value && makeDefault.value) {
+    controls.value = value
+  }
+  else {
+    controls.value = null
+  }
+})
+
+function addEventListeners() {
+  useEventListener(controlsRef.value as any, 'change', () => {
+    emit('change', controlsRef.value)
+    invalidate()
+  })
+  useEventListener(controlsRef.value as any, 'start', () => emit('start', controlsRef.value))
+  useEventListener(controlsRef.value as any, 'end', () => emit('end', controlsRef.value))
+}
+
+const { onBeforeRender } = useLoop()
+
+onBeforeRender(({ invalidate }) => {
+  if (controlsRef.value && (enableDamping.value || autoRotate.value)) {
+    controlsRef.value.update()
+
+    if (autoRotate.value) {
+      invalidate()
+    }
+  }
+})
+
+onUnmounted(() => {
+  if (controlsRef.value) {
+    controlsRef.value.dispose()
+  }
+})
+
+defineExpose({ instance: controlsRef })
+</script>
+
+<template>
+  <TresOrbitControls
+    v-if="(camera || activeCamera) && (domElement || renderer)"
+    ref="controlsRef"
+    :target="target"
+    :auto-rotate="autoRotate"
+    :auto-rotate-speed="autoRotateSpeed"
+    :enable-damping="enableDamping"
+    :damping-factor="dampingFactor"
+    :enable-pan="enablePan"
+    :key-pan-speed="keyPanSpeed"
+    :keys="keys"
+    :max-azimuth-angle="maxAzimuthAngle"
+    :min-azimuth-angle="minAzimuthAngle"
+    :max-polar-angle="maxPolarAngle"
+    :min-polar-angle="minPolarAngle"
+    :min-distance="minDistance"
+    :max-distance="maxDistance"
+    :min-zoom="minZoom"
+    :max-zoom="maxZoom"
+    :touches="touches"
+    :enable-zoom="enableZoom"
+    :zoom-speed="zoomSpeed"
+    :enable-rotate="enableRotate"
+    :rotate-speed="rotateSpeed"
+    :mouse-buttons="mouseButtons"
+    :args="[camera || activeCamera, domElement || renderer.domElement]"
+  />
+</template>

+ 9 - 0
playground/vue/src/pages/basic/PiercedProps.vue

@@ -1,6 +1,9 @@
 <script setup lang="ts">
 import { TresCanvas, useRenderLoop } from '@tresjs/core'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
 import { shallowRef } from 'vue'
+import LocalOrbitControls from '../../components/LocalOrbitControls.vue'
 
 const x = shallowRef(1)
 const y = shallowRef(1)
@@ -33,6 +36,10 @@ useRenderLoop().onLoop(({ elapsed }) => {
   refs[i].value = Math.cos(elapsed * Math.PI * 2)
   label.value = `${labels[i]} ${Math.trunc(refs[i].value * 10) / 10}`
 })
+
+const { enableZoom } = useControls({
+  enableZoom: false,
+})
 </script>
 
 <template>
@@ -40,6 +47,7 @@ useRenderLoop().onLoop(({ elapsed }) => {
     <p>Demonstrate pierced props</p>
     {{ label }}
   </div>
+  <TresLeches />
   <TresCanvas>
     <TresMesh
       :position-x="x"
@@ -55,6 +63,7 @@ useRenderLoop().onLoop(({ elapsed }) => {
       <TresBoxGeometry />
       <TresMeshNormalMaterial />
     </TresMesh>
+    <LocalOrbitControls :enable-zoom="enableZoom" />
   </TresCanvas>
 </template>
 

+ 1 - 1
src/core/nodeOps.test.ts

@@ -1343,7 +1343,7 @@ describe('nodeOps', () => {
         const s = v => JSON.stringify(v)
         const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
         const result = []
-        camera.position.set = (x, y, z) => result.push({ x, y, z })
+        camera.position.fromArray = ([x, y, z]: THREE.Vector3Tuple) => result.push({ x, y, z })
         nodeOps.patchProp(camera, 'position', undefined, [0, 0, 0])
         nodeOps.patchProp(camera, 'position', undefined, [1, 2, 3])
         nodeOps.patchProp(camera, 'position', undefined, [4, 5, 6])

+ 50 - 15
src/core/nodeOps.ts

@@ -4,7 +4,7 @@ import { BufferAttribute, Object3D } from 'three'
 import { isRef, type RendererOptions } from 'vue'
 import { attach, deepArrayEqual, doRemoveDeregister, doRemoveDetach, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance, setPrimitiveObject, unboxTresPrimitive } from '../utils'
 import { logError } from '../utils/logger'
-import { isArray, isCamera, isFunction, isObject, isObject3D, isScene, isTresInstance, isUndefined } from '../utils/is'
+import { isArray, isCamera, isClassInstance, isColor, isColorRepresentation, isCopyable, isFunction, isLayers, isObject, isObject3D, isScene, isTresInstance, isUndefined, isVectorLike } from '../utils/is'
 import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
 import { catalogue } from './catalogue'
 
@@ -243,7 +243,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
   function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
     if (!node) { return }
 
-    let root = node
+    let root: Record<string, unknown> = node
     let key = prop
 
     // NOTE: Update memoizedProps with the new value
@@ -278,7 +278,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       node.__tres.eventCount += 1
     }
     let finalKey = kebabToCamel(key)
-    let target = root?.[finalKey]
+    let target = root?.[finalKey] as Record<string, unknown>
 
     if (key === 'args') {
       const prevNode = node as TresObject3D
@@ -315,14 +315,14 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
           }
         })
 
-        root = prevNode
+        root = prevNode as TresObject
       }
       return
     }
 
     if (root.type === 'BufferGeometry') {
       if (key === 'args') { return }
-      root.setAttribute(
+      (root as TresObject).setAttribute(
         kebabToCamel(key),
         new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
       )
@@ -334,11 +334,12 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       // TODO: A standalone function called `resolve` is
       // available in /src/utils/index.ts. It's covered by tests.
       // Refactor below to DRY.
-      const chain = key.split('-')
-      target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
-      key = chain.pop() as string
-      finalKey = key
-      if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) }
+      target = root
+      for (const part of key.split('-')) {
+        finalKey = key = kebabToCamel(part)
+        root = target
+        target = target?.[key] as Record<string, unknown>
+      }
     }
     let value = nextValue
     if (value === '') { value = true }
@@ -357,11 +358,45 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       }
       return
     }
-    if (!target?.set && !isFunction(target)) { root[finalKey] = value }
-    else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
-    else if (isArray(value)) { target.set(...value) }
-    else if (!target.isColor && target.setScalar) { target.setScalar(value) }
-    else { target.set(value) }
+
+    // Layers must be written to the mask property
+    if (isLayers(target) && isLayers(value)) {
+      target.mask = value.mask
+    }
+    // Set colors if valid color representation for automatic conversion (copy)
+    else if (isColor(target) && isColorRepresentation(value)) {
+      target.set(value)
+    }
+    // Copy if properties match signatures and implement math interface (likely read-only)
+    else if (
+      isCopyable(target) && isClassInstance(value) && target.constructor === value.constructor
+    ) {
+      target.copy(value)
+    }
+    // Set array types
+    else if (isVectorLike(target) && Array.isArray(value)) {
+      if ('fromArray' in target && typeof target.fromArray === 'function') {
+        target.fromArray(value)
+      }
+      else {
+        target.set(...value)
+      }
+    }
+    // Set literal types
+    else if (isVectorLike(target) && typeof value === 'number') {
+      // Allow setting array scalars
+      if ('setScalar' in target && typeof target.setScalar === 'function') {
+        target.setScalar(value)
+      }
+      // Otherwise just set single value
+      else {
+        target.set(value)
+      }
+    }
+    // Else, just overwrite the value
+    else {
+      root[finalKey] = value
+    }
 
     if (isCamera(node)) {
       node.updateProjectionMatrix()

+ 29 - 1
src/utils/is.ts

@@ -1,5 +1,6 @@
 import type { TresCamera, TresInstance, TresObject, TresPrimitive } from 'src/types'
-import type { BufferGeometry, Fog, Light, Material, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
+import type { BufferGeometry, Color, ColorRepresentation, Fog, Light, Material, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
+import { Layers } from 'three'
 
 /**
  * Type guard to check if a value is undefined
@@ -163,6 +164,33 @@ export function isCamera(value: unknown): value is TresCamera {
   return isObject(value) && !!(value.isCamera)
 }
 
+export function isColor(value: unknown): value is Color {
+  return isObject(value) && !!(value.isColor)
+}
+
+export function isColorRepresentation(value: unknown): value is ColorRepresentation {
+  return value != null && (typeof value === 'string' || typeof value === 'number' || isColor(value))
+}
+
+interface VectorLike { set: (...args: any[]) => void, constructor?: (...args: any[]) => any }
+export function isVectorLike(value: unknown): value is VectorLike {
+  return value !== null && typeof value === 'object' && 'set' in value && typeof value.set === 'function'
+}
+
+interface Copyable { copy: (...args: any[]) => void, constructor?: (...args: any[]) => any }
+export function isCopyable(value: unknown): value is Copyable {
+  return isVectorLike(value) && 'copy' in value && typeof value.copy === 'function'
+}
+
+interface ClassInstance { constructor?: (...args: any[]) => any }
+export function isClassInstance(object: unknown): object is ClassInstance {
+  return !!(object)?.constructor
+}
+
+export function isLayers(value: unknown): value is Layers {
+  return value instanceof Layers // three does not implement .isLayers
+}
+
 /**
  * Type guard to check if a value is a Three.js OrthographicCamera
  * @param value - The value to check