diff --git a/ARKit.js b/ARKit.js index 6d85c689..01bafec5 100644 --- a/ARKit.js +++ b/ARKit.js @@ -5,9 +5,6 @@ // Copyright © 2017 HippoAR. All rights reserved. // -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - import { StyleSheet, View, @@ -15,7 +12,10 @@ import { NativeModules, requireNativeComponent, } from 'react-native'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { pickColors, pickColorsFromFile } from './lib/pickColors'; import generateId from './components/lib/generateId'; const ARKitManager = NativeModules.ARKitManager; @@ -36,9 +36,7 @@ class ARKit extends Component { reason: 0, floor: null, }; - componentWillMount() { - ARKitManager.clearScene(); - } + componentDidMount() { ARKitManager.resume(); } @@ -172,12 +170,19 @@ ARKit.exportModel = presetId => { return ARKitManager.exportModel(property).then(result => ({ ...result, id })); }; +ARKit.pickColors = pickColors; +ARKit.pickColorsFromFile = pickColorsFromFile; ARKit.propTypes = { debug: PropTypes.bool, planeDetection: PropTypes.bool, - lightEstimation: PropTypes.bool, + lightEstimationEnabled: PropTypes.bool, + autoenablesDefaultLighting: PropTypes.bool, + worldAlignment: PropTypes.number, onPlaneDetected: PropTypes.func, onFeaturesDetected: PropTypes.func, + // onLightEstimation is called rapidly, better poll with + // ARKit.getCurrentLightEstimation() + onLightEstimation: PropTypes.func, onPlaneUpdate: PropTypes.func, onTrackingState: PropTypes.func, onTapOnPlaneUsingExtent: PropTypes.func, @@ -187,4 +192,4 @@ ARKit.propTypes = { const RCTARKit = requireNativeComponent('RCTARKit', ARKit); -module.exports = ARKit; +export default ARKit; diff --git a/DeviceMotion.js b/DeviceMotion.js index 94a86d25..aa8041fd 100644 --- a/DeviceMotion.js +++ b/DeviceMotion.js @@ -16,4 +16,4 @@ const DeviceMotion = { }, }; -module.exports = DeviceMotion; +export default DeviceMotion; diff --git a/README.md b/README.md index 726bfaa1..cdcfe47d 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,29 @@ There is a Slack group that anyone can join for help / support / general questio ## Getting started -`$ npm install react-native-arkit --save` +`$ yarn add react-native-arkit` + +make sure to use the latest version of yarn (>=1.x.x) + +(npm does not work properly at the moment. See https://github.com/HippoAR/react-native-arkit/issues/103) + ### Mostly automatic installation `$ react-native link react-native-arkit` +! Currently automatic installation does not work as PocketSVG is missing. Follow the manual installation + ### Manual installation #### iOS 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` -2. Go to `node_modules` ➜ `react-native-arkit` and add `RCTARKit.xcodeproj` -3. In XCode, in the project navigator, select your project. Add `libRCTARKit.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` -4. Run your project (`Cmd+R`)< +2. Go to `node_modules` ➜ add `react-native-arkit/RCTARKit.xcodeproj` and `_PocketSVG/_PocketSVG.xcodeproj` +3. In XCode, in the project navigator, select your project. Add `libRCTARKit.a` `and PocketSVG.framework` to your project's `Build Phases` ➜ `Link Binary With Libraries` +4. In Tab `General` ➜ `Embedded Binaries` ➜ `+` ➜ Add `PocketSVG.framework ios` +5. Run your project (`Cmd+R`)< ## Usage @@ -53,7 +61,12 @@ export default class ReactNativeARKit extends Component { style={{ flex: 1 }} debug planeDetection - lightEstimation + // enable light estimation (defaults to true) + lightEstimationEnabled + // get the current lightEstimation (if enabled) + // it fires rapidly, so better poll it from outside with + // ARKit.getCurrentLightEstimation() + onLightEstimation={e => console.log(e.nativeEvent)} onPlaneDetected={console.log} // event listener for plane detection onPlaneUpdate={console.log} // event listener for plane update > @@ -98,13 +111,47 @@ export default class ReactNativeARKit extends Component { position={{ x: 0.2, y: 0.6, z: 0 }} font={{ size: 0.15, depth: 0.05 }} /> + + + + + `, + pathFlatness: 0.1, + // it's also possible to specify a chamfer profile: + chamferRadius: 5, + chamferProfilePathSvg: ` + + `, + extrusion: 10, + }} + /> ); @@ -127,152 +174,338 @@ AppRegistry.registerComponent('ReactNativeARKit', () => ReactNativeARKit); |---|---|---|---| | `debug` | `Boolean` | `false` | Debug mode will show the 3D axis and feature points detected. | `planeDetection` | `Boolean` | `false` | ARKit plane detection. -| `lightEstimation` | `Boolean` | `false` | ARKit light estimation. +| `lightEstimationEnabled` | `Boolean` | `false` | ARKit light estimation. +| `worldAlignment` | `Enumeration`
One of: `ARKit.ARWorldAlignment.Gravity`, `ARKit.ARWorldAlignment.GravityAndHeading`, `ARKit.ARWorldAlignment.Camera` (documentation [here](https://developer.apple.com/documentation/arkit/arworldalignment)) | `ARKit.ARWorldAlignment.Gravity` | **ARWorldAlignmentGravity**
The coordinate system's y-axis is parallel to gravity, and its origin is the initial position of the device. **ARWorldAlignmentGravityAndHeading**
The coordinate system's y-axis is parallel to gravity, its x- and z-axes are oriented to compass heading, and its origin is the initial position of the device. **ARWorldAlignmentCamera**
The scene coordinate system is locked to match the orientation of the camera.| ##### Events | Event Name | Returns | Notes |---|---|---| | `onPlaneDetected` | `{ id, center, extent }` | When a plane is first detected. +| `onLightEstimation` | `{ ambientColorTemperature, ambientIntensity }` | Light estimation on every frame. Called rapidly, better use polling. See `ARKit.getCurrentLightEstimation()` +| `onFeaturesDetected` | `{ featurePoints}` | Detected Features on every frame (currently also not throttled). Usefull to display custom dots for detected features. You can also poll this information with `ARKit.getCurrentDetectedFeaturePoints()` | `onPlaneUpdate` | `{ id, center, extent }` | When a detected plane is updated ##### Static methods +All methods return a promise with the result. + | Method Name | Arguments | Notes |---|---|---| | `snapshot` | | | Take a screenshot (will save to Photo Library) | | `snapshotCamera` | | Take a screenshot without 3d models (will save to Photo Library) | | `getCameraPosition` | | Get the current position of the `ARCamera` | +| `getCurrentLightEstimation` | | Get current light estimation `{ ambientColorTemperature, ambientIntensity}` | +| `getCurrentDetectedFeaturePoints` | | Get current detected feature points (in last current frame) (array) | | `focusScene` | | Sets the scene's position/rotation to where it was when first rendered (but now relative to your device's current position/rotation) | | `hitTestPlanes` | point, type | check if a plane has ben hit by point (`{x,y}`) with detection type (any of `ARKit.ARHitTestResultType`). See https://developer.apple.com/documentation/arkit/arhittestresulttype?language=objc for further information | | `hitTestSceneObjects` | point | check if a scene object has ben hit by point (`{x,y}`) | +#### 3D objects + +##### General props + +Most 3d object have these common properties + +| Prop | Type | Description | +|---|---|---| +| `position` | `{ x, y, z }` | The object's position (y is up) | +| `scale` | Number | The scale of the object. Defaults to 1 | +| `eulerAngles` | `{ x, y, z }` | The rotation in eulerAngles | +| `rotation` | TODO | see scenkit documentation | +| `orientation` | TODO | see scenkit documentation | +| `shape` | depends on object | the shape of the object (will probably renamed to geometry in future versions) +| `material` | `{ diffuse, metalness, roughness, lightingModel, shaders }` | the material of the object | +| `transition` | `{duration: 1}` | Some property changes can be animated like in css transitions. Currently you can specify the duration (in seconds). | +| `renderingOrder` | Number | Order in which object is rendered. Usefull to place elements "behind" others, although they are nearer. | +| `categoryBitMask` | Number / bitmask | control which lights affect this object | +| `castsShadow` | `boolean` | whether this object casts hadows | + +*New experimental feature:* + +You can switch properties on mount or onmount by specifying `propsOnMount` and `propsOnUnmount`. +E.g. you can scale an object on unmount: + +``` + +``` + +#### Material properties + +Most objects take a material property with these sub-props: + +| Prop | Type | Description | +|---|---|---| +| `diffuse` | { `path`, `color`, `intensity` } | [diffuse](https://developer.apple.com/documentation/scenekit/scnmaterial/1462589-diffuse?language=objc) +| `specular` | { `path`, `color`, `intensity` } | [specular](https://developer.apple.com/documentation/scenekit/scnmaterial/1462516-specular?language=objc) +| `displacement` | { `path`, `color`, `intensity` } | [displacement](https://developer.apple.com/documentation/scenekit/scnmaterial/2867516-displacement?language=objc) +| `normal` | { `path`, `color`, `intensity` } | [normal](https://developer.apple.com/documentation/scenekit/scnmaterial/1462542-normal) +| `metalness` | number | metalness of the object | +| `roughness` | number | roughness of the object | +| `doubleSided` | boolean | render both sides, default is `true` | +| `litPerPixel` | boolean | calculate lighting per-pixel or vertex [litPerPixel](https://developer.apple.com/documentation/scenekit/scnmaterial/1462580-litperpixel) | +| `lightingModel` | `ARKit.LightingModel.*` | [LightingModel](https://developer.apple.com/documentation/scenekit/scnmaterial.lightingmodel) | +| `blendMode` | `ARKit.BlendMode.*` | [BlendMode](https://developer.apple.com/documentation/scenekit/scnmaterial/1462585-blendmode) | +| `fillMode` | `ARKit.FillMode.*` | [FillMode](https://developer.apple.com/documentation/scenekit/scnmaterial/2867442-fillmode) +| `shaders` | Object with keys from `ARKit.ShaderModifierEntryPoint.*` and shader strings as values | [Shader modifiers](https://developer.apple.com/documentation/scenekit/scnshadable) | +| `colorBufferWriteMask` | `ARKit.ColorMask.*` | [color mask](https://developer.apple.com/documentation/scenekit/scncolormask). Set to ARKit.ColorMask.None so that an object is transparent, but receives deferred shadows. | + + + + #### [``](https://developer.apple.com/documentation/scenekit/scnbox) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ width, height, length, chamfer }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | + +And any common object property (position, material, etc.) #### [``](https://developer.apple.com/documentation/scenekit/scnsphere) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ radius }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | + + #### [``](https://developer.apple.com/documentation/scenekit/scncylinder) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ radius, height }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scncone) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ topR, bottomR, height }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scnpyramid) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ width, height, length }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scntube) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ innerR, outerR, height }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scntorus) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ ringR, pipeR }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scncapsule) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `shape` | `{ capR, height }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | #### [``](https://developer.apple.com/documentation/scenekit/scnplane) -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | -| `shape` | `{ width, length }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | +| `shape` | `{ width, height }` | -#### [``](https://developer.apple.com/documentation/scenekit/scntext) +Notice: planes are veritcally aligned. If you want a horizontal plane, rotate it around the x-axis. -##### Props +*Example*: + +This is a horizontal plane that only receives shadows, but is invisible otherwise: + +``` + +``` + + +#### [``](https://developer.apple.com/documentation/scenekit/scntext) | Prop | Type | |---|---| | `text` | `String` | -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `font` | `{ name, size, depth, chamfer }` | -| `material` | `{ diffuse, metalness, roughness, lightingModel }` | + #### `` SceneKit only supports `.scn` and `.dae` formats. -##### Props | Prop | Type | |---|---| -| `position` | `{ x, y, z }` | -| `eulerAngles` | `{ x, y, z }` | | `model` | `{ file, node, scale, alpha }` | +Objects currently don't take material property. + +#### `` + +Creates a extruded shape by an svg path. +See https://github.com/HippoAR/react-native-arkit/pull/89 for details + +| Prop | Type | +|---|---| +| `shape` | `{ pathSvg, extrusion, pathFlatness, chamferRadius, chamferProfilePathSvg, chamferProfilePathFlatness }` | + + + +#### [``](https://developer.apple.com/documentation/scenekit/scnlight) + +Place lights on the scene! + +You might set `autoenablesDefaultLighting={false}` on The `` component to disable default lighting. You can use `lightEstimationEnabled` and `ARKit.getCurrentLightEstimation()` to find values for intensity and temperature. This produces much nicer results then `autoenablesDefaultLighting`. + + + +| Prop | Type | Description | +|---|---|---| +| `position` | `{ x, y, z }` | | +| `eulerAngles` | `{ x, y, z }` | | +| `type` | any of `ARKit.LightType` | see [here for details](https://developer.apple.com/documentation/scenekit/scnlight.lighttype) | +| `color` | `string` | the color of the light | +| `temperature` | `Number` | The color temperature of the light | +| `intensity` | `Number` | The light intensity | +| `lightCategoryBitMask` | `Number`/`bitmask` | control which objects are lit by this light | +| `castsShadow` | `boolean` | whether to cast shadows on object | +| `shadowMode`| `ARKit.ShadowMode.* | Define the shadowmode. Set to `ARKit.ShadowMode.Deferred` to cast shadows on invisible objects (like an invisible floor plane) | + + + +Most properties described here are also supported: https://developer.apple.com/documentation/scenekit/scnlight + +This feature is new. If you experience any problem, please report an issue! + + +### HOCs (higher order components) + +#### withProjectedPosition() + +this hoc allows you to create 3D components where the position is always relative to the same point on the screen/camera, but sticks to a plane or object. + +Think about a 3D cursor that can be moved across your table or a 3D cursor on a wall. + +You can use the hoc like this: + +``` +const Cursor3D = withProjectedPosition()(({positionProjected, projectionResult}) => { + if(!projectionResult) { + // nothing has been hit, don't render it + return null; + } + return ( + + ) +}) + +``` + +It's recommended that you specify a transition duration (0.1s works nice), as the position gets updated rapidly, but slightly throttled. + +Now you can use your 3D cursor like this: + +##### Attach to a given detected horizontal plane + +Given you have detected a plane with onPlaneDetected, you can make the cursor stick to that plane: + +``` + + +``` + +If you don't have the id, but want to place the cursor on a certain plane (e.g. the first or last one), pass a function for plane. This function will get all hit-results and you can return the one you need: + +``` + results.length > 0 ? results[0] : null + }} +/> + +``` + +You can also add a property `onProjectedPosition` to your cursor which will be called with the hit result on every frame + +It uses https://developer.apple.com/documentation/arkit/arframe/2875718-hittest with some default options. Please file an issue or send a PR if you need more control over the options here! + +##### Attach to a given 3D object + +You can attach the cursor on a 3D object, e.g. a non-horizontal-plane or similar: + +Given there is some 3D object on your scene with `id="my-nodeId"` + +``` + +``` + +Like with planes, you can select the node with a function. + +E.gl you have several "walls" with ids "wall_1", "wall_2", etc. + +``` + results.find(r => r.id.startsWith('wall_')), + }} +/> +``` + + +It uses https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522929-hittest with some default options. Please file an issue or send a PR if you need more control over the options here! + + ## Contributing diff --git a/components/ARBox.js b/components/ARBox.js index a673f0c9..b53350ce 100644 --- a/components/ARBox.js +++ b/components/ARBox.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARBox = createArComponent('addBox', { @@ -16,6 +17,7 @@ const ARBox = createArComponent('addBox', { length: PropTypes.number, chamfer: PropTypes.number, }), + material, }); -module.exports = ARBox; +export default ARBox; diff --git a/components/ARCapsule.js b/components/ARCapsule.js index c647189e..b74df957 100644 --- a/components/ARCapsule.js +++ b/components/ARCapsule.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARCapsule = createArComponent('addCapsule', { @@ -14,6 +15,7 @@ const ARCapsule = createArComponent('addCapsule', { capR: PropTypes.number, height: PropTypes.number, }), + material, }); -module.exports = ARCapsule; +export default ARCapsule; diff --git a/components/ARCone.js b/components/ARCone.js index d446d9c0..01eef2b0 100644 --- a/components/ARCone.js +++ b/components/ARCone.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARCone = createArComponent('addCone', { @@ -15,6 +16,7 @@ const ARCone = createArComponent('addCone', { bottomR: PropTypes.number, height: PropTypes.number, }), + material, }); -module.exports = ARCone; +export default ARCone; diff --git a/components/ARCylinder.js b/components/ARCylinder.js index b69093de..ccbd1887 100644 --- a/components/ARCylinder.js +++ b/components/ARCylinder.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARCylinder = createArComponent('addCylinder', { @@ -14,6 +15,7 @@ const ARCylinder = createArComponent('addCylinder', { radius: PropTypes.number, height: PropTypes.number, }), + material, }); -module.exports = ARCylinder; +export default ARCylinder; diff --git a/components/ARGroup.js b/components/ARGroup.js index f9b45999..4371a4a7 100644 --- a/components/ARGroup.js +++ b/components/ARGroup.js @@ -7,4 +7,4 @@ const ARGroup = class extends Component { } }; -module.exports = ARGroup; +export default ARGroup; diff --git a/components/ARImage.js b/components/ARImage.js deleted file mode 100644 index e69de29b..00000000 diff --git a/components/ARLight.js b/components/ARLight.js new file mode 100644 index 00000000..0328e0e9 --- /dev/null +++ b/components/ARLight.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; + +import { categoryBitMask, color, lightType, shadowMode } from './lib/propTypes'; +import createArComponent from './lib/createArComponent'; + +const ARLight = createArComponent('addLight', { + type: lightType, + color, + temperature: PropTypes.number, + intensity: PropTypes.number, + attenuationStartDistance: PropTypes.number, + attenuationEndDistance: PropTypes.number, + spotInnerAngle: PropTypes.number, + spotOuterAngle: PropTypes.number, + castsShadow: PropTypes.bool, + shadowRadius: PropTypes.number, + shadowColor: color, + // shadowMapSize: PropTypes.number, + shadowSampleCount: PropTypes.number, + shadowMode, + shadowBias: PropTypes.number, + orthographicScale: PropTypes.number, + zFar: PropTypes.number, + zNear: PropTypes.number, + lightCategoryBitMask: categoryBitMask, +}); + +export default ARLight; diff --git a/components/ARModel.js b/components/ARModel.js index b018b7d7..2103cc74 100644 --- a/components/ARModel.js +++ b/components/ARModel.js @@ -15,7 +15,6 @@ import createArComponent from './lib/createArComponent'; const ARModel = createArComponent( { mount: NativeModules.ARModelManager.mount, - pick: ['model', 'material', 'shape'], }, { model: PropTypes.shape({ @@ -26,6 +25,7 @@ const ARModel = createArComponent( }), material, }, + ['model'], ); -module.exports = ARModel; +export default ARModel; diff --git a/components/ARPlane.js b/components/ARPlane.js index 04ce9de1..10cbe90e 100644 --- a/components/ARPlane.js +++ b/components/ARPlane.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARPlane = createArComponent('addPlane', { @@ -18,6 +19,7 @@ const ARPlane = createArComponent('addPlane', { widthSegmentCount: PropTypes.number, heightSegmentCount: PropTypes.number, }), + material, }); -module.exports = ARPlane; +export default ARPlane; diff --git a/components/ARPyramid.js b/components/ARPyramid.js index fc91c8ab..90f47acc 100644 --- a/components/ARPyramid.js +++ b/components/ARPyramid.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARPyramid = createArComponent('addPyramid', { @@ -15,6 +16,7 @@ const ARPyramid = createArComponent('addPyramid', { length: PropTypes.number, height: PropTypes.number, }), + material, }); -module.exports = ARPyramid; +export default ARPyramid; diff --git a/components/ARShape.js b/components/ARShape.js new file mode 100644 index 00000000..91e951f9 --- /dev/null +++ b/components/ARShape.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; + +import { chamferMode, material } from './lib/propTypes'; +import createArComponent from './lib/createArComponent'; + +const ARShape = createArComponent('addShape', { + shape: PropTypes.shape({ + extrusion: PropTypes.number, + pathSvg: PropTypes.string, + pathFlatness: PropTypes.number, + chamferMode, + chamferRadius: PropTypes.number, + chamferProfilePathSvg: PropTypes.string, + chamferProfilePathFlatness: PropTypes.number, + }), + material, +}); + +export default ARShape; diff --git a/components/ARSphere.js b/components/ARSphere.js index eaccb10e..31e31269 100644 --- a/components/ARSphere.js +++ b/components/ARSphere.js @@ -7,12 +7,14 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARSphere = createArComponent('addSphere', { shape: PropTypes.shape({ radius: PropTypes.number, }), + material, }); -module.exports = ARSphere; +export default ARSphere; diff --git a/components/ARSprite.js b/components/ARSprite.js index f44022af..bcb8be64 100644 --- a/components/ARSprite.js +++ b/components/ARSprite.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import withAnimationFrame from 'react-animation-frame'; +import withAnimationFrame from '@panter/react-animation-frame'; import { NativeModules, Animated } from 'react-native'; @@ -13,7 +13,7 @@ const ARSprite = withAnimationFrame( super(props); this.state = { zIndex: new Animated.Value(), - pos2D: new Animated.ValueXY(), // inits to zero + pos2D: new Animated.ValueXY() // inits to zero }; } onAnimationFrame() { @@ -22,9 +22,9 @@ const ARSprite = withAnimationFrame( { x: this.state.pos2D.x, y: this.state.pos2D.y, - z: this.state.zIndex, - }, - ]), + z: this.state.zIndex + } + ]) ); } @@ -34,18 +34,18 @@ const ARSprite = withAnimationFrame( style={{ position: 'absolute', transform: this.state.pos2D.getTranslateTransform(), - ...this.props.style, + ...this.props.style }} > {this.props.children} ); } - }, + } ); ARSprite.propTypes = { - position, + position }; -module.exports = ARSprite; +export default ARSprite; diff --git a/components/ARText.js b/components/ARText.js index a856ca16..207d0ab8 100644 --- a/components/ARText.js +++ b/components/ARText.js @@ -9,10 +9,11 @@ import PropTypes from 'prop-types'; import { NativeModules } from 'react-native'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARText = createArComponent( - { mount: NativeModules.ARTextManager.mount, pick: ['text', 'font'] }, + { mount: NativeModules.ARTextManager.mount, pick: ['id', 'text', 'font'] }, { text: PropTypes.string, font: PropTypes.shape({ @@ -20,9 +21,11 @@ const ARText = createArComponent( // weight: PropTypes.string, size: PropTypes.number, depth: PropTypes.number, - chamfer: PropTypes.number, + chamfer: PropTypes.number }), + material }, + ['text', 'font'] ); -module.exports = ARText; +export default ARText; diff --git a/components/ARTorus.js b/components/ARTorus.js index 25857193..07c926eb 100644 --- a/components/ARTorus.js +++ b/components/ARTorus.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARTorus = createArComponent('addTorus', { @@ -14,6 +15,7 @@ const ARTorus = createArComponent('addTorus', { ringR: PropTypes.number, pipeR: PropTypes.number, }), + material, }); -module.exports = ARTorus; +export default ARTorus; diff --git a/components/ARTube.js b/components/ARTube.js index 91a594dc..ba203bb4 100644 --- a/components/ARTube.js +++ b/components/ARTube.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; +import { material } from './lib/propTypes'; import createArComponent from './lib/createArComponent'; const ARTube = createArComponent('addTube', { @@ -15,6 +16,7 @@ const ARTube = createArComponent('addTube', { outerR: PropTypes.number, height: PropTypes.number, }), + material, }); -module.exports = ARTube; +export default ARTube; diff --git a/components/lib/createArComponent.js b/components/lib/createArComponent.js index f48ecd78..e10e4404 100644 --- a/components/lib/createArComponent.js +++ b/components/lib/createArComponent.js @@ -1,4 +1,5 @@ import { Component } from 'react'; +import { NativeModules, processColor } from 'react-native'; import PropTypes from 'prop-types'; import filter from 'lodash/filter'; import isDeepEqual from 'fast-deep-equal'; @@ -7,45 +8,79 @@ import keys from 'lodash/keys'; import pick from 'lodash/pick'; import some from 'lodash/some'; -import { NativeModules } from 'react-native'; - import { + castsShadow, + categoryBitMask, eulerAngles, - material, + opacity, orientation, position, + renderingOrder, rotation, + scale, transition, } from './propTypes'; -import { processColorInMaterial } from './parseColor'; +import processMaterial from './processMaterial'; import generateId from './generateId'; -const ARGeosManager = NativeModules.ARGeosManager; -const NODE_PROPS = [ - 'position', - 'eulerAngles', - 'rotation', - 'orientation', - 'transition', -]; -const KEYS_THAT_NEED_REMOUNT = ['material', 'shape', 'model']; - -const nodeProps = (id, props) => ({ - id, - ...pick(props, NODE_PROPS), -}); - -export default (mountConfig, propTypes = {}) => { - const getShapeAndMaterialProps = props => - typeof mountConfig === 'string' - ? { - shape: props.shape, - material: processColorInMaterial(props.material), - } - : { - ...pick(props, mountConfig.pick), - material: processColorInMaterial(props.material), - }; +const { ARGeosManager } = NativeModules; + +const PROP_TYPES_IMMUTABLE = { + id: PropTypes.string, + frame: PropTypes.string, +}; +const MOUNT_UNMOUNT_ANIMATION_PROPS = { + propsOnMount: PropTypes.any, + propsOnUnMount: PropTypes.any, +}; +const PROP_TYPES_NODE = { + position, + transition, + orientation, + eulerAngles, + rotation, + scale, + categoryBitMask, + castsShadow, + renderingOrder, + opacity, +}; + +const NODE_PROPS = keys(PROP_TYPES_NODE); +const IMMUTABLE_PROPS = keys(PROP_TYPES_IMMUTABLE); +const DEBUG = false; +const TIMERS = {}; + +/** +mountConfig, +propTypes, +nonUpdateablePropKeys: if a prop key is in this list, +the property will be updated on scenekit, instead of beeing remounted. + +this excludes at the moment: model, font, text, (???) +* */ +export default (mountConfig, propTypes = {}, nonUpdateablePropKeys = []) => { + const allPropTypes = { + ...MOUNT_UNMOUNT_ANIMATION_PROPS, + ...PROP_TYPES_IMMUTABLE, + ...PROP_TYPES_NODE, + ...propTypes, + }; + // any custom props (material, shape, ...) + const nonNodePropKeys = keys(propTypes); + + const parseMaterials = props => ({ + ...props, + ...(props.shadowColor + ? { shadowColor: processColor(props.shadowColor) } + : {}), + ...(props.material ? { material: processMaterial(props.material) } : {}), + }); + + const getNonNodeProps = props => ({ + ...pick(props, nonNodePropKeys), + ...parseMaterials(props), + }); const mountFunc = typeof mountConfig === 'string' @@ -54,18 +89,36 @@ export default (mountConfig, propTypes = {}) => { const mount = (id, props) => { mountFunc( - getShapeAndMaterialProps(props), - nodeProps(id, props), + getNonNodeProps(props), + { + id, + ...pick(props, NODE_PROPS), + }, props.frame, ); }; const ARComponent = class extends Component { identifier = null; - componentDidMount() { this.identifier = this.props.id || generateId(); - mount(this.identifier, this.props); + const { propsOnMount, ...props } = this.props; + if (propsOnMount) { + const fullPropsOnMount = { ...props, ...propsOnMount }; + const { + transition: transitionOnMount = { duration: 0 }, + } = fullPropsOnMount; + if (DEBUG) console.log('mount', fullPropsOnMount); + this.doPendingTimers(); + mount(this.identifier, fullPropsOnMount); + + this.delayed(() => { + this.props = propsOnMount; + this.componentWillUpdate({ ...props, transition: transitionOnMount }); + }, transitionOnMount.duration * 1000); + } else { + mount(this.identifier, props); + } } componentWillUpdate(props) { @@ -78,38 +131,83 @@ export default (mountConfig, propTypes = {}) => { return; } - if (some(KEYS_THAT_NEED_REMOUNT, k => changedKeys.includes(k))) { - // remount - // TODO: we should be able to update + if (__DEV__) { + const nonAllowedUpdates = filter(changedKeys, k => + IMMUTABLE_PROPS.includes(k), + ); + if (nonAllowedUpdates.length > 0) { + throw new Error( + `prop can't be updated: '${nonAllowedUpdates.join(', ')}'`, + ); + } + } - mount(this.identifier, props); + if (some(changedKeys, k => nonUpdateablePropKeys.includes(k))) { + if (DEBUG) console.log('need to remount node because of ', changedKeys); + mount(this.identifier, { ...this.props, ...props }); } else { - // always include transition - ARGeosManager.update( - this.identifier, - pick(props, ['transition', ...changedKeys]), - ); + // every property is updateable + // send only these changed property to the native part + + const propsToupdate = { + // always inclue transition + transition: { + ...this.props.transition, + ...props.transition, + }, + ...parseMaterials(pick(props, changedKeys)), + }; + + if (DEBUG) console.log('update node', propsToupdate); + ARGeosManager.updateNode(this.identifier, propsToupdate); } } componentWillUnmount() { - ARGeosManager.unmount(this.identifier); + const { propsOnUnmount, ...props } = this.props; + if (propsOnUnmount) { + const fullProps = { ...props, ...propsOnUnmount }; + const { transition: { duration = 0 } = {} } = fullProps; + + this.componentWillUpdate(fullProps); + this.delayed(() => { + ARGeosManager.unmount(this.identifier); + }, duration * 1000); + } else { + this.doPendingTimers(); + ARGeosManager.unmount(this.identifier); + } + } + /** + do something delayed, but keep order of events per id + * */ + delayed(callback, duration) { + this.doPendingTimers(); + TIMERS[this.identifier] = { + handle: global.setTimeout(() => { + callback.call(this); + delete TIMERS[this.identifier]; + }, duration), + callback, + }; + } + + doPendingTimers() { + if (TIMERS[this.identifier]) { + // timer is running, do it now, otherwise we might change order + // e.g. it could be that an unmount happens after a remount + global.clearTimeout(TIMERS[this.identifier].handle); + TIMERS[this.identifier].callback.call(this); + delete TIMERS[this.identifier]; + } } render() { return null; } }; - ARComponent.propTypes = { - frame: PropTypes.string, - position, - transition, - eulerAngles, - rotation, - orientation, - material, - ...propTypes, - }; + + ARComponent.propTypes = allPropTypes; return ARComponent; }; diff --git a/components/lib/generateId.js b/components/lib/generateId.js index e932303f..50956e66 100644 --- a/components/lib/generateId.js +++ b/components/lib/generateId.js @@ -3,6 +3,7 @@ const digits = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; function toSex(num, base) { + /* eslint no-param-reassign:0 */ if (base) { num = parseInt(num, base); } diff --git a/components/lib/parseColor.js b/components/lib/parseColor.js deleted file mode 100644 index 7f67d5aa..00000000 --- a/components/lib/parseColor.js +++ /dev/null @@ -1,16 +0,0 @@ -import { processColor } from 'react-native'; - -export function processColorInMaterial(material) { - if (!material) { - return material; - } - - if (!material.diffuse && !material.color) { - return material; - } - - return { - ...material, - diffuse: processColor(material.diffuse || material.color), - }; -} diff --git a/components/lib/processMaterial.js b/components/lib/processMaterial.js new file mode 100644 index 00000000..055a43f4 --- /dev/null +++ b/components/lib/processMaterial.js @@ -0,0 +1,26 @@ +import { processColor } from 'react-native'; +import { isString, mapValues, set } from 'lodash'; + +// https://developer.apple.com/documentation/scenekit/scnmaterial +const propsWithMaps = ['normal', 'diffuse', 'displacement', 'specular']; + +export default function processMaterial(material) { + // previously it was possible to set { material: { color:'colorstring'}}... translate this to { material: { diffuse: { color: 'colorstring'}}} + if (material.color) { + set(material, 'diffuse.color', material.color); + } + + return mapValues( + material, + (prop, key) => + propsWithMaps.includes(key) + ? { + ...prop, + color: processColor( + // allow for setting a diffuse colorstring { diffuse: 'colorstring'} + key === 'diffuse' && isString(prop) ? prop : prop.color, + ), + } + : prop, + ); +} diff --git a/components/lib/propTypes.js b/components/lib/propTypes.js index 97a48d45..e8c84202 100644 --- a/components/lib/propTypes.js +++ b/components/lib/propTypes.js @@ -1,8 +1,7 @@ +import { NativeModules } from 'react-native'; import { values } from 'lodash'; import PropTypes from 'prop-types'; -import { NativeModules } from 'react-native'; - const ARKitManager = NativeModules.ARKitManager; export const position = PropTypes.shape({ @@ -10,6 +9,9 @@ export const position = PropTypes.shape({ y: PropTypes.number, z: PropTypes.number, }); + +export const scale = PropTypes.number; +export const categoryBitMask = PropTypes.number; export const transition = PropTypes.shape({ duration: PropTypes.number, }); @@ -44,13 +46,42 @@ export const lightingModel = PropTypes.oneOf( values(ARKitManager.LightingModel), ); +export const castsShadow = PropTypes.bool; +export const renderingOrder = PropTypes.number; export const blendMode = PropTypes.oneOf(values(ARKitManager.BlendMode)); +export const chamferMode = PropTypes.oneOf(values(ARKitManager.ChamferMode)); +export const color = PropTypes.string; +export const fillMode = PropTypes.oneOf(values(ARKitManager.FillMode)); -export const material = PropTypes.shape({ +export const lightType = PropTypes.oneOf(values(ARKitManager.LightType)); +export const shadowMode = PropTypes.oneOf(values(ARKitManager.ShadowMode)); +export const colorBufferWriteMask = PropTypes.oneOf( + values(ARKitManager.ColorMask), +); + +export const opacity = PropTypes.number; + +export const materialProperty = PropTypes.shape({ + path: PropTypes.string, color: PropTypes.string, + intensity: PropTypes.number, +}); + +export const material = PropTypes.shape({ + color, + normal: materialProperty, + specular: materialProperty, + displacement: materialProperty, + diffuse: PropTypes.oneOfType([PropTypes.string, materialProperty]), metalness: PropTypes.number, roughness: PropTypes.number, blendMode, lightingModel, shaders, + writesToDepthBuffer: PropTypes.bool, + colorBufferWriteMask, + doubleSided: PropTypes.bool, + litPerPixel: PropTypes.bool, + transparency: PropTypes.number, + fillMode, }); diff --git a/hocs/withProjectedPosition.js b/hocs/withProjectedPosition.js new file mode 100644 index 00000000..ffc86790 --- /dev/null +++ b/hocs/withProjectedPosition.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; +import withAnimationFrame from '@panter/react-animation-frame'; +import { round, isFunction } from 'lodash'; +import { NativeModules } from 'react-native'; + +const ARKitManager = NativeModules.ARKitManager; + +const roundPoint = ({ x, y, z }, precision) => ({ + x: round(x, precision), + y: round(y, precision), + z: round(z, precision), +}); + +export default ({ throttleMs = 33 } = {}) => C => + withAnimationFrame( + class extends Component { + projectionRunning = true; + _isMounted = false; + constructor(props) { + super(props); + this.state = { + positionProjected: props.position || { x: 0, y: 0, z: 0 }, + projectionResult: null, + }; + this.handleAnimation(props); + } + componentWillMount() { + this._isMounted = true; + } + handleAnimation({ projectionEnabled }) { + if (projectionEnabled) { + if (!this.projectionRunning) { + this.projectionRunning = true; + this.props.startAnimation(); + } + } else if (this.projectionRunning) { + this.projectionRunning = false; + this.props.endAnimation(); + } + } + componentWillUpdate({ projectionEnabled = true }) { + this.handleAnimation({ projectionEnabled }); + } + + componentWillUnmount() { + this._isMounted = false; + } + + onResult(result) { + if (this._isMounted) { + if (result) { + this.setState({ + positionProjected: roundPoint(result.point, 3), + projectionResult: result, + }); + } else { + this.setState({ + positionProjected: null, + projectionResult: null, + }); + } + if (this.props.onProjectedPosition) { + this.props.onProjectedPosition(result); + } + } + } + + onAnimationFrame() { + const { x, y, plane, node } = this.props.projectPosition || {}; + + if (plane) { + ARKitManager.hitTestPlanes( + { x, y }, + ARKitManager.ARHitTestResultType.ExistingPlane, + ).then(({ results }) => { + const result = isFunction(plane) + ? plane(results) + : results.find(r => r.id === plane); + this.onResult(result); + }); + } else if (node) { + ARKitManager.hitTestSceneObjects({ x, y }).then(({ results }) => { + const result = isFunction(node) + ? node(results) + : results.find(r => r.id === node); + this.onResult(result); + }); + } + } + + render() { + return ( + + ); + } + }, + throttleMs, + ); diff --git a/index.js b/index.js index d54139eb..0bdb4b4d 100644 --- a/index.js +++ b/index.js @@ -5,22 +5,27 @@ // Copyright © 2017 HippoAR. All rights reserved. // -import ARKit from './ARKit'; -import DeviceMotion from './DeviceMotion'; - import ARBox from './components/ARBox'; -import ARSphere from './components/ARSphere'; -import ARCylinder from './components/ARCylinder'; -import ARCone from './components/ARCone'; -import ARPyramid from './components/ARPyramid'; -import ARTube from './components/ARTube'; -import ARTorus from './components/ARTorus'; import ARCapsule from './components/ARCapsule'; -import ARPlane from './components/ARPlane'; -import ARText from './components/ARText'; +import ARCone from './components/ARCone'; +import ARCylinder from './components/ARCylinder'; +import ARGroup from './components/ARGroup'; +import ARKit from './ARKit'; +import ARLight from './components/ARLight'; import ARModel from './components/ARModel'; +import ARPlane from './components/ARPlane'; +import ARPyramid from './components/ARPyramid'; +import ARShape from './components/ARShape'; +import ARSphere from './components/ARSphere'; import ARSprite from './components/ARSprite'; -import ARGroup from './components/ARGroup'; +import ARText from './components/ARText'; +import ARTorus from './components/ARTorus'; +import ARTube from './components/ARTube'; +import DeviceMotion from './DeviceMotion'; +import startup from './startup'; +import withProjectedPosition from './hocs/withProjectedPosition'; + +import * as colorUtils from './lib/colorUtils'; ARKit.Box = ARBox; ARKit.Sphere = ARSphere; @@ -35,12 +40,18 @@ ARKit.Text = ARText; ARKit.Model = ARModel; ARKit.Sprite = ARSprite; ARKit.Group = ARGroup; +ARKit.Shape = ARShape; +ARKit.Light = ARLight; + +startup(); -module.exports = { +export { + colorUtils, ARKit, DeviceMotion, ARBox, ARSphere, + ARSprite, ARCylinder, ARCone, ARPyramid, @@ -50,5 +61,7 @@ module.exports = { ARPlane, ARText, ARModel, + ARLight, ARGroup, + withProjectedPosition, }; diff --git a/ios/RCTARKit.h b/ios/RCTARKit.h index 34033e8b..793a85f4 100644 --- a/ios/RCTARKit.h +++ b/ios/RCTARKit.h @@ -35,10 +35,13 @@ typedef void (^RCTARKitReject)(NSString *code, NSString *message, NSError *error @property (nonatomic, assign) BOOL debug; @property (nonatomic, assign) BOOL planeDetection; -@property (nonatomic, assign) BOOL lightEstimation; +@property (nonatomic, assign) BOOL lightEstimationEnabled; +@property (nonatomic, assign) BOOL autoenablesDefaultLighting; +@property (nonatomic, assign) ARWorldAlignment worldAlignment; @property (nonatomic, copy) RCTBubblingEventBlock onPlaneDetected; @property (nonatomic, copy) RCTBubblingEventBlock onFeaturesDetected; +@property (nonatomic, copy) RCTBubblingEventBlock onLightEstimation; @property (nonatomic, copy) RCTBubblingEventBlock onPlaneUpdate; @property (nonatomic, copy) RCTBubblingEventBlock onTrackingState; @property (nonatomic, copy) RCTBubblingEventBlock onTapOnPlaneUsingExtent; @@ -58,12 +61,14 @@ typedef void (^RCTARKitReject)(NSString *code, NSString *message, NSError *error - (void)hitTestSceneObjects:(CGPoint)tapPoint resolve:(RCTARKitResolve) resolve reject:(RCTARKitReject)reject; - (SCNVector3)projectPoint:(SCNVector3)point; - (float)getCameraDistanceToPoint:(SCNVector3)point; -- (UIImage *)getSnaphshot; -- (UIImage *)getSnaphshotCamera; +- (UIImage *)getSnapshot:(NSDictionary*)selection; +- (UIImage *)getSnapshotCamera:(NSDictionary*)selection; - (void)focusScene; - (void)clearScene; - (NSDictionary *)readCameraPosition; - (NSDictionary *)readCamera; +- (NSDictionary* )getCurrentLightEstimation; +- (NSArray * )getCurrentDetectedFeaturePoints; diff --git a/ios/RCTARKit.m b/ios/RCTARKit.m index 365c155a..4bc4db72 100644 --- a/ios/RCTARKit.m +++ b/ios/RCTARKit.m @@ -74,6 +74,7 @@ - (instancetype)initWithARView:(ARSCNView *)arView { // configuration(s) arView.autoenablesDefaultLighting = YES; + arView.scene.rootNode.name = @"root"; self.planes = [NSMutableDictionary new]; @@ -135,12 +136,12 @@ - (void)setDebug:(BOOL)debug { } - (BOOL)planeDetection { - ARWorldTrackingConfiguration *configuration = (ARWorldTrackingConfiguration *) self.session.configuration; + ARWorldTrackingConfiguration *configuration = (ARWorldTrackingConfiguration *) self.configuration; return configuration.planeDetection == ARPlaneDetectionHorizontal; } - (void)setPlaneDetection:(BOOL)planeDetection { - ARWorldTrackingConfiguration *configuration = (ARWorldTrackingConfiguration *) self.session.configuration; + ARWorldTrackingConfiguration *configuration = (ARWorldTrackingConfiguration *) self.configuration; if (planeDetection) { configuration.planeDetection = ARPlaneDetectionHorizontal; } else { @@ -149,14 +150,39 @@ - (void)setPlaneDetection:(BOOL)planeDetection { [self resume]; } -- (BOOL)lightEstimation { - ARConfiguration *configuration = self.session.configuration; +- (BOOL)lightEstimationEnabled { + ARConfiguration *configuration = self.configuration; return configuration.lightEstimationEnabled; } -- (void)setLightEstimation:(BOOL)lightEstimation { - ARConfiguration *configuration = self.session.configuration; - configuration.lightEstimationEnabled = lightEstimation; + +- (void)setLightEstimationEnabled:(BOOL)lightEstimationEnabled { + ARConfiguration *configuration = self.configuration; + configuration.lightEstimationEnabled = lightEstimationEnabled; + [self resume]; +} +- (void)setAutoenablesDefaultLighting:(BOOL)autoenablesDefaultLighting { + self.arView.autoenablesDefaultLighting = autoenablesDefaultLighting; +} + +- (BOOL)autoenablesDefaultLighting { + return self.arView.autoenablesDefaultLighting; +} + +- (ARWorldAlignment)worldAlignment { + ARConfiguration *configuration = self.configuration; + return configuration.worldAlignment; +} + +- (void)setWorldAlignment:(ARWorldAlignment)worldAlignment { + ARConfiguration *configuration = self.configuration; + if (worldAlignment == ARWorldAlignmentGravityAndHeading) { + configuration.worldAlignment = ARWorldAlignmentGravityAndHeading; + } else if (worldAlignment == ARWorldAlignmentCamera) { + configuration.worldAlignment = ARWorldAlignmentCamera; + } else { + configuration.worldAlignment = ARWorldAlignmentGravity; + } [self resume]; } @@ -179,12 +205,13 @@ - (NSDictionary *)readCamera { SCNVector4 rotation = self.nodeManager.cameraOrigin.rotation; SCNVector4 orientation = self.nodeManager.cameraOrigin.orientation; SCNVector3 eulerAngles = self.nodeManager.cameraOrigin.eulerAngles; + SCNVector3 direction = self.nodeManager.cameraDirection; return @{ @"position":vectorToJson(position), @"rotation":vector4ToJson(rotation), @"orientation":vector4ToJson(orientation), @"eulerAngles":vectorToJson(eulerAngles), - + @"direction":vectorToJson(direction), }; } @@ -192,21 +219,10 @@ - (SCNVector3)projectPoint:(SCNVector3)point { return [self.arView projectPoint:point]; } -static float getDistance(const SCNVector3 pointA, const SCNVector3 pointB) { - float xd = pointB.x - pointA.x; - float yd = pointB.y - pointA.y; - float zd = pointB.z - pointA.z; - float distance = sqrt(xd * xd + yd * yd + zd * zd); - - if (distance < 0){ - return (distance * -1); - } else { - return (distance); - } -} + - (float)getCameraDistanceToPoint:(SCNVector3)point { - return getDistance(self.nodeManager.cameraOrigin.position, point); + return [self.nodeManager getCameraDistanceToPoint:point]; } @@ -223,7 +239,6 @@ -(ARWorldTrackingConfiguration *)configuration { _configuration = [ARWorldTrackingConfiguration new]; _configuration.planeDetection = ARPlaneDetectionHorizontal; - return _configuration; } @@ -237,16 +252,19 @@ - (void)hitTestSceneObjects:(const CGPoint)tapPoint resolve:(RCTARKitResolve)res } -- (UIImage *)getSnaphshot { +- (UIImage *)getSnapshot:(NSDictionary *)selection { UIImage *image = [self.arView snapshot]; - return image; + + + return [self cropImage:image toSelection:selection]; + } -- (UIImage *)getSnaphsotCamera { +- (UIImage *)getSnapshotCamera:(NSDictionary *)selection { CVPixelBufferRef pixelBuffer = self.arView.session.currentFrame.capturedImage; CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; @@ -259,11 +277,102 @@ - (UIImage *)getSnaphsotCamera { UIImage *image = [UIImage imageWithCGImage:videoImage scale: 1.0 orientation:UIImageOrientationRight]; CGImageRelease(videoImage); - return image; + + UIImage *cropped = [self cropImage:image toSelection:selection]; + return cropped; + } +- (UIImage *)cropImage:(UIImage *)imageToCrop toRect:(CGRect)rect +{ + //CGRect CropRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height+15); + + CGImageRef imageRef = CGImageCreateWithImageInRect([imageToCrop CGImage], rect); + UIImage *cropped = [UIImage imageWithCGImage:imageRef]; + CGImageRelease(imageRef); + + return cropped; +} + +static inline double radians (double degrees) {return degrees * M_PI/180;} +UIImage* rotate(UIImage* src, UIImageOrientation orientation) +{ + UIGraphicsBeginImageContext(src.size); + + CGContextRef context = UIGraphicsGetCurrentContext(); + [src drawAtPoint:CGPointMake(0, 0)]; + if (orientation == UIImageOrientationRight) { + CGContextRotateCTM (context, radians(90)); + } else if (orientation == UIImageOrientationLeft) { + CGContextRotateCTM (context, radians(-90)); + } else if (orientation == UIImageOrientationDown) { + // NOTHING + } else if (orientation == UIImageOrientationUp) { + CGContextRotateCTM (context, radians(90)); + } + + + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} +- (UIImage *)cropImage:(UIImage *)imageToCrop toSelection:(NSDictionary *)selection +{ + + // selection is in view-coordinate system + // where as the image is a camera picture with arbitary size + // also, the camera picture is cut of so that it "covers" the self.bounds + // if selection is nil, crop to the viewport + + UIImage * image = rotate(imageToCrop, imageToCrop.imageOrientation); + + float arViewWidth = self.bounds.size.width; + float arViewHeight = self.bounds.size.height; + float imageWidth = image.size.width; + float imageHeight = image.size.height; + + float arViewRatio = arViewHeight/arViewWidth; + float imageRatio = imageHeight/imageWidth; + float imageToArWidth = imageWidth/arViewWidth; + float imageToArHeight = imageHeight/arViewHeight; + + float finalHeight; + float finalWidth; + + + if (arViewRatio > imageRatio) + { + finalHeight = arViewHeight*imageToArHeight; + finalWidth = arViewHeight*imageToArHeight /arViewRatio; + } + else + { + finalWidth = arViewWidth*imageToArWidth; + finalHeight = arViewWidth * imageToArWidth * arViewRatio; + } + + float topOffset = (image.size.height - finalHeight)/2; + float leftOffset = (image.size.width - finalWidth)/2; + + + float x = leftOffset; + float y = topOffset; + float width = finalWidth; + float height = finalHeight; + if(selection && selection != [NSNull null]) { + x = leftOffset+ [selection[@"x"] floatValue]*imageToArWidth; + y = topOffset+[selection[@"y"] floatValue]*imageToArHeight; + width = [selection[@"width"] floatValue]*imageToArWidth; + height = [selection[@"height"] floatValue]*imageToArHeight; + } + CGRect rect = CGRectMake(x, y, width, height); + + UIImage *cropped = [self cropImage:image toRect:rect]; + return cropped; +} #pragma mark - plane hit detection @@ -277,8 +386,10 @@ - (void)hitTestPlane:(const CGPoint)tapPoint types:(ARHitTestResultType)types re NSMutableArray *resultsMapped = [NSMutableArray arrayWithCapacity:[results count]]; [results enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { ARHitTestResult *result = (ARHitTestResult *) obj; + [resultsMapped addObject:(@{ @"distance": @(result.distance), + @"id": result.anchor.identifier.UUIDString, @"point": @{ @"x": @(result.worldTransform.columns[3].x), @"y": @(result.worldTransform.columns[3].y), @@ -392,21 +503,6 @@ - (void)renderer:(id )renderer willUpdateNode:(SCNNode *)node - (void)renderer:(id )renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor { ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; - SCNNode *parent = [node parentNode]; - // NSLog(@"%@", parent.name); - // NSLog(@"%f %f %f", node.position.x, node.position.y, node.position.z); - // NSLog(@"%f %f %f %f", node.rotation.x, node.rotation.y, node.rotation.z, node.rotation.w); - - - // NSLog(@"%@", @{ - // @"id": planeAnchor.identifier.UUIDString, - // @"alignment": @(planeAnchor.alignment), - // @"node": @{ @"x": @(node.position.x), @"y": @(node.position.y), @"z": @(node.position.z) }, - // @"center": @{ @"x": @(planeAnchor.center.x), @"y": @(planeAnchor.center.y), @"z": @(planeAnchor.center.z) }, - // @"extent": @{ @"x": @(planeAnchor.extent.x), @"y": @(planeAnchor.extent.y), @"z": @(planeAnchor.extent.z) }, - // @"camera": @{ @"x": @(self.cameraOrigin.position.x), @"y": @(self.cameraOrigin.position.y), @"z": @(self.cameraOrigin.position.z) } - // }); - if (self.onPlaneUpdate) { self.onPlaneUpdate(@{ @"id": planeAnchor.identifier.UUIDString, @@ -434,6 +530,32 @@ - (void)renderer:(id )renderer didRemoveNode:(SCNNode *)node f #pragma mark - ARSessionDelegate +- (ARFrame * _Nullable)currentFrame { + return self.arView.session.currentFrame; +} + +- (NSDictionary *)getCurrentLightEstimation { + return [self wrapLightEstimation:[self currentFrame].lightEstimate]; +} + +- (NSMutableArray *)getCurrentDetectedFeaturePoints { + NSMutableArray * featurePoints = [NSMutableArray array]; + for (int i = 0; i < [self currentFrame].rawFeaturePoints.count; i++) { + vector_float3 point = [self currentFrame].rawFeaturePoints.points[i]; + + NSString * pointId = [NSString stringWithFormat:@"featurepoint_%lld",[self currentFrame].rawFeaturePoints.identifiers[i]]; + + [featurePoints addObject:@{ + @"x": @(point[0]), + @"y": @(point[1]), + @"z": @(point[2]), + @"id":pointId, + }]; + + } + return featurePoints; +} + - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame { for (id sessionDelegate in self.sessionDelegates) { if ([sessionDelegate respondsToSelector:@selector(session:didUpdateFrame:)]) { @@ -441,20 +563,7 @@ - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame { } } if (self.onFeaturesDetected) { - NSMutableArray * featurePoints = [NSMutableArray array]; - for (int i = 0; i < frame.rawFeaturePoints.count; i++) { - vector_float3 point = frame.rawFeaturePoints.points[i]; - - NSString * pointId = [NSString stringWithFormat:@"featurepoint_%lld",frame.rawFeaturePoints.identifiers[i]]; - - [featurePoints addObject:@{ - @"x": @(point[0]), - @"y": @(point[1]), - @"z": @(point[2]), - @"id":pointId, - }]; - - } + NSMutableArray * featurePoints = [self getCurrentDetectedFeaturePoints]; dispatch_async(dispatch_get_main_queue(), ^{ @@ -465,6 +574,31 @@ - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame { } }); } + + if (self.lightEstimationEnabled && self.onLightEstimation) { + /** this is called rapidly and is therefore demanding, better poll it from outside with getCurrentLightEstimation **/ + + + + dispatch_async(dispatch_get_main_queue(), ^{ + if(self.onLightEstimation) { + NSDictionary *estimate = [self getCurrentLightEstimation]; + self.onLightEstimation(estimate); + } + }); + + } + +} + +- (NSDictionary *)wrapLightEstimation:(ARLightEstimate *)estimate { + if(!estimate) { + return nil; + } + return @{ + @"ambientColorTemperature":@(estimate.ambientColorTemperature), + @"ambientIntensity":@(estimate.ambientIntensity), + }; } diff --git a/ios/RCTARKit.xcodeproj/project.pbxproj b/ios/RCTARKit.xcodeproj/project.pbxproj index aa4ab109..29e9ce49 100644 --- a/ios/RCTARKit.xcodeproj/project.pbxproj +++ b/ios/RCTARKit.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 10ED47A71F38BC01004DF043 /* DeviceMotion.m in Sources */ = {isa = PBXBuildFile; fileRef = 10ED47A61F38BC00004DF043 /* DeviceMotion.m */; }; 10FEF6141F774C9000EC21AE /* RCTARKitIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 10FEF6101F774C8F00EC21AE /* RCTARKitIO.m */; }; 10FEF6151F774C9000EC21AE /* RCTARKitNodes.m in Sources */ = {isa = PBXBuildFile; fileRef = 10FEF6121F774C9000EC21AE /* RCTARKitNodes.m */; }; + B1990B221FCEEBD60001AE2F /* color-grabber.m in Sources */ = {isa = PBXBuildFile; fileRef = B1990B211FCEEBD60001AE2F /* color-grabber.m */; }; B3E7B58A1CC2AC0600A0062D /* RCTARKit.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RCTARKit.m */; }; /* End PBXBuildFile section */ @@ -29,6 +30,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + B14C363A1F9504D70047CB67 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -52,6 +62,10 @@ 10FEF6121F774C9000EC21AE /* RCTARKitNodes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTARKitNodes.m; sourceTree = ""; }; 10FEF6131F774C9000EC21AE /* RCTARKitDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTARKitDelegate.h; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRCTARKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTARKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; + B14C36631F960C500047CB67 /* PocketSVG.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PocketSVG.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B14C36651F960C6E0047CB67 /* PocketSVG.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PocketSVG.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B1990B201FCEEBD60001AE2F /* color-grabber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "color-grabber.h"; sourceTree = ""; }; + B1990B211FCEEBD60001AE2F /* color-grabber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "color-grabber.m"; sourceTree = ""; }; B3E7B5881CC2AC0600A0062D /* RCTARKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTARKit.h; sourceTree = ""; }; B3E7B5891CC2AC0600A0062D /* RCTARKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTARKit.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -91,6 +105,7 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + B1990B1F1FCEEBD60001AE2F /* color-grabber */, 106999E21F3EC2FB00032829 /* components */, 10ED47A51F38BC00004DF043 /* DeviceMotion.h */, 10ED47A61F38BC00004DF043 /* DeviceMotion.m */, @@ -108,7 +123,26 @@ 105F124C1F7C0718006D4BA3 /* RCTConvert+ARKit.h */, 105F124D1F7C0718006D4BA3 /* RCTConvert+ARKit.m */, 134814211AA4EA7D00B7C361 /* Products */, + B13FF7601F94F72400A6C92B /* Frameworks */, + ); + sourceTree = ""; + }; + B13FF7601F94F72400A6C92B /* Frameworks */ = { + isa = PBXGroup; + children = ( + B14C36651F960C6E0047CB67 /* PocketSVG.framework */, + B14C36631F960C500047CB67 /* PocketSVG.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B1990B1F1FCEEBD60001AE2F /* color-grabber */ = { + isa = PBXGroup; + children = ( + B1990B201FCEEBD60001AE2F /* color-grabber.h */, + B1990B211FCEEBD60001AE2F /* color-grabber.m */, ); + path = "color-grabber"; sourceTree = ""; }; /* End PBXGroup section */ @@ -121,6 +155,7 @@ 58B511D71A9E6C8500147676 /* Sources */, 58B511D81A9E6C8500147676 /* Frameworks */, 58B511D91A9E6C8500147676 /* CopyFiles */, + B14C363A1F9504D70047CB67 /* CopyFiles */, ); buildRules = ( ); @@ -170,6 +205,7 @@ 10FEF6141F774C9000EC21AE /* RCTARKitIO.m in Sources */, 10DCBC4B1F7CE836008C89E7 /* ARGeosManager.m in Sources */, 10FEF6151F774C9000EC21AE /* RCTARKitNodes.m in Sources */, + B1990B221FCEEBD60001AE2F /* color-grabber.m in Sources */, 10E553291F1391350059B7EC /* Plane.m in Sources */, 1021FE1C1F3EDB98000E7339 /* ARModelManager.m in Sources */, 1021FE1D1F3EDB9B000E7339 /* ARTextManager.m in Sources */, @@ -269,6 +305,8 @@ "$(SRCROOT)/../../react-native-arcl/ios", ../../../ios/Pods/Headers/Public/, ../../../ios/Pods/Headers/Public/React, + "$(SRCROOT)/../../_PocketSVG/**", + "../node_modules/_PocketSVG/**", ); IPHONEOS_DEPLOYMENT_TARGET = 11.0; LIBRARY_SEARCH_PATHS = "$(inherited)"; @@ -290,6 +328,8 @@ "$(SRCROOT)/../../react-native-arcl/ios", ../../../ios/Pods/Headers/Public/, ../../../ios/Pods/Headers/Public/React, + "$(SRCROOT)/../../_PocketSVG/**", + "../node_modules/_PocketSVG/**", ); IPHONEOS_DEPLOYMENT_TARGET = 11.0; LIBRARY_SEARCH_PATHS = "$(inherited)"; diff --git a/ios/RCTARKitManager.m b/ios/RCTARKitManager.m index 94d83311..6d7ab838 100644 --- a/ios/RCTARKitManager.m +++ b/ios/RCTARKitManager.m @@ -11,6 +11,7 @@ #import "RCTARKitNodes.h" #import #import +#import "color-grabber.h" @implementation RCTARKitManager @@ -37,6 +38,28 @@ - (NSDictionary *)constantsToExport @"Phong": SCNLightingModelPhong, @"PhysicallyBased": SCNLightingModelPhysicallyBased }, + @"LightType": @{ + @"Ambient": SCNLightTypeAmbient, + @"Directional": SCNLightTypeDirectional, + @"Omni": SCNLightTypeOmni, + @"Probe": SCNLightTypeProbe, + @"Spot": SCNLightTypeSpot, + @"IES": SCNLightTypeIES + }, + @"ShadowMode": @{ + @"Forward": [@(SCNShadowModeForward) stringValue], + @"Deferred": [@(SCNShadowModeDeferred) stringValue], + @"ModeModulated": [@(SCNShadowModeModulated) stringValue], + }, + @"ColorMask": @{ + @"All": [@(SCNColorMaskAll) stringValue], + @"None": [@(SCNColorMaskNone) stringValue], + @"Alpha": [@(SCNColorMaskAlpha) stringValue], + @"Blue": [@(SCNColorMaskBlue) stringValue], + @"Red": [@(SCNColorMaskRed) stringValue], + @"Green": [@(SCNColorMaskGreen) stringValue], + }, + @"ShaderModifierEntryPoint": @{ @"Geometry": SCNShaderModifierEntryPointGeometry, @"Surface": SCNShaderModifierEntryPointSurface, @@ -51,18 +74,36 @@ - (NSDictionary *)constantsToExport @"Screen": [@(SCNBlendModeScreen) stringValue], @"Replace": [@(SCNBlendModeReplace) stringValue], + }, + @"ChamferMode": @{ + @"Both": [@(SCNChamferModeBoth) stringValue], + @"Back": [@(SCNChamferModeBack) stringValue], + @"Front": [@(SCNChamferModeBack) stringValue], + + }, + @"ARWorldAlignment": @{ + @"Gravity": @(ARWorldAlignmentGravity), + @"GravityAndHeading": @(ARWorldAlignmentGravityAndHeading), + @"Camera": @(ARWorldAlignmentCamera), + }, + @"FillMode": @{ + @"Fill": [@(SCNFillModeFill) stringValue], + @"Lines": [@(SCNFillModeLines) stringValue], } }; } RCT_EXPORT_VIEW_PROPERTY(debug, BOOL) RCT_EXPORT_VIEW_PROPERTY(planeDetection, BOOL) -RCT_EXPORT_VIEW_PROPERTY(lightEstimation, BOOL) +RCT_EXPORT_VIEW_PROPERTY(lightEstimationEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(autoenablesDefaultLighting, BOOL) +RCT_EXPORT_VIEW_PROPERTY(worldAlignment, NSInteger) RCT_EXPORT_VIEW_PROPERTY(onPlaneDetected, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPlaneUpdate, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTrackingState, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFeaturesDetected, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onLightEstimation, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTapOnPlaneUsingExtent, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTapOnPlaneNoExtent, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onEvent, RCTBubblingEventBlock) @@ -114,7 +155,7 @@ - (NSString *)getAssetUrl:(NSString *)localID { return assetURLStr; } -- (void)storeImageInPhotoAlbum:(UIImage *)image reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve { +- (void)storeImageInPhotoAlbum:(UIImage *)image cameraProperties:(NSDictionary *) cameraProperties reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve { __block PHObjectPlaceholder *placeholder; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ @@ -129,7 +170,7 @@ - (void)storeImageInPhotoAlbum:(UIImage *)image reject:(RCTPromiseRejectBlock)re NSString * assetURLStr = [self getAssetUrl:localID]; - resolve(@{@"url": assetURLStr, @"width":@(image.size.width), @"height": @(image.size.height)}); + resolve(@{@"url": assetURLStr, @"width":@(image.size.width), @"height": @(image.size.height), @"camera":cameraProperties}); } else { @@ -139,7 +180,7 @@ - (void)storeImageInPhotoAlbum:(UIImage *)image reject:(RCTPromiseRejectBlock)re } -- (void)storeImageInDirectory:(UIImage *)image directory:(NSString *)directory format:(NSString *)format reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve { +- (void)storeImageInDirectory:(UIImage *)image directory:(NSString *)directory format:(NSString *)format cameraProperties:(NSDictionary *) cameraProperties reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve { NSData *data; if([format isEqualToString:@"jpg"]) { data = UIImageJPEGRepresentation(image, 0.9); @@ -157,7 +198,7 @@ - (void)storeImageInDirectory:(UIImage *)image directory:(NSString *)directory f NSString *filePath = [directory stringByAppendingPathComponent:uniqueFileName]; //Add the file name bool success = [data writeToFile:filePath atomically:YES]; //Write the file if(success) { - resolve(@{@"url": filePath, @"width":@(image.size.width), @"height": @(image.size.height)}); + resolve(@{@"url": filePath, @"width":@(image.size.width), @"height": @(image.size.height), @"camera":cameraProperties}); } else { // TODO use NSError from writeToFile reject(@"snapshot_error", [NSString stringWithFormat:@"could not save to '%@'", filePath], nil); @@ -165,7 +206,7 @@ - (void)storeImageInDirectory:(UIImage *)image directory:(NSString *)directory f } -- (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve { +- (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTPromiseRejectBlock)reject resolve:(RCTPromiseResolveBlock)resolve cameraProperties:(NSDictionary *)cameraProperties { NSString * target = @"cameraRoll"; NSString * format = @"png"; @@ -177,7 +218,7 @@ - (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTP } if([target isEqualToString:@"cameraRoll"]) { // camera roll / photo album - [self storeImageInPhotoAlbum:image reject:reject resolve:resolve]; + [self storeImageInPhotoAlbum:image cameraProperties:cameraProperties reject:reject resolve:resolve ]; } else { NSString * dir; if([target isEqualToString:@"cache"]) { @@ -188,21 +229,46 @@ - (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTP } else { dir = target; } - [self storeImageInDirectory:image directory:dir format:format reject:reject resolve:resolve]; + [self storeImageInDirectory:image directory:dir format:format cameraProperties:cameraProperties reject:reject resolve:resolve ]; } } RCT_EXPORT_METHOD(snapshot:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - UIImage *image = [[ARKit sharedInstance] getSnaphshot]; - [self storeImage:image options:options reject:reject resolve:resolve]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSDictionary * selection = options[@"selection"]; + NSDictionary * cameraProperties = [[ARKit sharedInstance] readCamera]; + UIImage *image = [[ARKit sharedInstance] getSnapshot:selection]; + + [self storeImage:image options:options reject:reject resolve:resolve cameraProperties:cameraProperties ]; + }); } RCT_EXPORT_METHOD(snapshotCamera:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - UIImage *image = [[ARKit sharedInstance] getSnaphshotCamera]; - [self storeImage:image options:options reject:reject resolve:resolve]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSDictionary * selection = options[@"selection"]; + NSDictionary * cameraProperties = [[ARKit sharedInstance] readCamera]; + UIImage *image = [[ARKit sharedInstance] getSnapshotCamera:selection]; + [self storeImage:image options:options reject:reject resolve:resolve cameraProperties:cameraProperties]; + }); +} + +RCT_EXPORT_METHOD(pickColorsRaw:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + NSDictionary * selection = options[@"selection"]; + UIImage *image = [[ARKit sharedInstance] getSnapshotCamera:selection]; + resolve([[ColorGrabber sharedInstance] getColorsFromImage:image options:options]); + }); +} + +RCT_EXPORT_METHOD(pickColorsRawFromFile:(NSString * )filePath options:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + UIImage *image = [UIImage imageWithContentsOfFile:filePath]; + resolve([[ColorGrabber sharedInstance] getColorsFromImage:image options:options]); + }); } RCT_EXPORT_METHOD(getCamera:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { @@ -213,6 +279,14 @@ - (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTP resolve([[ARKit sharedInstance] readCameraPosition]); } +RCT_EXPORT_METHOD(getCurrentLightEstimation:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + resolve([[ARKit sharedInstance] getCurrentLightEstimation]); +} + +RCT_EXPORT_METHOD(getCurrentDetectedFeaturePoints:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + resolve([[ARKit sharedInstance] getCurrentDetectedFeaturePoints]); +} + RCT_EXPORT_METHOD(projectPoint: (NSDictionary *)pointDict resolve:(RCTPromiseResolveBlock)resolve @@ -240,4 +314,3 @@ - (void)storeImage:(UIImage *)image options:(NSDictionary *)options reject:(RCTP } @end - diff --git a/ios/RCTARKitNodes.h b/ios/RCTARKitNodes.h index c67fa67f..c3136bcc 100644 --- a/ios/RCTARKitNodes.h +++ b/ios/RCTARKitNodes.h @@ -36,7 +36,8 @@ typedef NS_OPTIONS(NSUInteger, RFReferenceFrame) { + (instancetype)sharedInstance; - (void)addNodeToScene:(SCNNode *)node inReferenceFrame:(NSString *)referenceFrame; -- (void)updateNode:(NSString *)key properties:(NSDictionary *) properties; +- (void)updateNode:(NSString *)nodeId properties:(NSDictionary *) properties; +- (float)getCameraDistanceToPoint:(SCNVector3)point; - (void)registerNode:(SCNNode *)node forKey:(NSString *)key; - (SCNNode *)nodeForKey:(NSString *)key; - (void)removeNodeForKey:(NSString *)key; diff --git a/ios/RCTARKitNodes.m b/ios/RCTARKitNodes.m index 27754820..a7f4a98c 100644 --- a/ios/RCTARKitNodes.m +++ b/ios/RCTARKitNodes.m @@ -107,16 +107,15 @@ - (void)clear { - (void)addNodeToLocalFrame:(SCNNode *)node { node.referenceFrame = RFReferenceFrameLocal; - NSLog(@"[RCTARKitNodes] Add model %@ to Local frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); - + //NSLog(@"[RCTARKitNodes] Add node %@ to Local frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); + [self registerNode:node forKey:node.name]; [self.localOrigin addChildNode:node]; } - (void)addNodeToCameraFrame:(SCNNode *)node { node.referenceFrame = RFReferenceFrameCamera; - - NSLog(@"[RCTARKitNodes] Add model %@ to Camera frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); + //NSLog(@"[RCTARKitNodes] Add node %@ to Camera frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); [self registerNode:node forKey:node.name]; [self.cameraOrigin addChildNode:node]; } @@ -124,7 +123,7 @@ - (void)addNodeToCameraFrame:(SCNNode *)node { - (void)addNodeToFrontOfCameraFrame:(SCNNode *)node { node.referenceFrame = RFReferenceFrameFrontOfCamera; - NSLog(@"[RCTARKitNodes] Add model %@ to FrontOfCamera frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); + //NSLog(@"[RCTARKitNodes] Add node %@ to FrontOfCamera frame at (%.2f, %.2f, %.2f)", node.name, node.position.x, node.position.y, node.position.z); [self registerNode:node forKey:node.name]; [self.frontOfCamera addChildNode:node]; } @@ -132,12 +131,13 @@ - (void)addNodeToFrontOfCameraFrame:(SCNNode *)node { - (NSDictionary *)getSceneObjectsHitResult:(const CGPoint)tapPoint { NSDictionary *options = @{ - SCNHitTestRootNodeKey: self.localOrigin + SCNHitTestRootNodeKey: self.localOrigin, + SCNHitTestSortResultsKey: @(YES) }; - NSArray *results = [_arView hitTest:tapPoint options:options]; + NSArray *results = [_arView hitTest:tapPoint options:options]; NSMutableArray * resultsMapped = [self mapHitResultsWithSceneResults:results]; - NSDictionary *planeHitResult = getSceneObjectHitResult(resultsMapped, tapPoint); - return planeHitResult; + NSDictionary *result = getSceneObjectHitResult(resultsMapped, tapPoint); + return result; } @@ -155,24 +155,37 @@ - (NSDictionary *)getSceneObjectsHitResult:(const CGPoint)tapPoint { - (NSMutableArray *) mapHitResultsWithSceneResults: (NSArray *)results { NSMutableArray *resultsMapped = [NSMutableArray arrayWithCapacity:[results count]]; + [results enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { SCNHitTestResult *result = (SCNHitTestResult *) obj; SCNNode * node = result.node; - NSArray *keys = [self.nodes allKeysForObject: node]; - if([keys count]) { - - NSString * firstKey = [keys firstObject]; + + NSString * nodeId = [self findNodeId:node]; + if(nodeId) { + + SCNVector3 point = result.worldCoordinates; + SCNVector3 normal = result.worldNormal; + float distance = [self getCameraDistanceToPoint:point]; + [resultsMapped addObject:(@{ - @"id": firstKey + @"id": nodeId, + @"distance": @(distance), + @"point": @{ + @"x": @(point.x), + @"y": @(point.y), + @"z": @(point.z) + }, + @"normal": @{ + @"x": @(normal.x), + @"y": @(normal.y), + @"z": @(normal.z) + + } } )]; - } else { - NSLog(@"no key found for node %@", node); - NSLog(@"for results %@", results); - NSLog(@"all nodes %@", self.nodes); - NSLog(@"origin %@", self.localOrigin); } - + }]; + return resultsMapped; } @@ -187,28 +200,64 @@ - (void)registerNode:(SCNNode *)node forKey:(NSString *)key { } } + +- (NSString *) findNodeId:(SCNNode *)nodeWithParents { + + SCNNode* _node = nodeWithParents; + while(_node) { + if(_node.name && [self.nodes objectForKey:_node.name]) { + return _node.name; + } + _node = _node.parentNode; + } + return nil; + +} + + - (SCNNode *)nodeForKey:(NSString *)key { return [self.nodes objectForKey:key]; } - (void)removeNodeForKey:(NSString *)key { + SCNNode *node = [self.nodes objectForKey:key]; if (node) { - [node removeFromParentNode]; [self.nodes removeObjectForKey:key]; + if(node.light) { + // see https://stackoverflow.com/questions/47270056/how-to-remove-a-light-with-shadowmode-deferred-in-scenekit-arkit?noredirect=1#comment81491270_47270056 + node.hidden = YES; + [node removeFromParentNode]; + } else { + [node removeFromParentNode]; + } } } -- (void)updateNode:(NSString *)key properties:(NSDictionary *) properties { - SCNNode *node = [self.nodes objectForKey:key]; - // only basic properties like position and rotation can currently be updated this way +- (void)updateNode:(NSString *)nodeId properties:(NSDictionary *) properties { + SCNNode *node = [self.nodes objectForKey:nodeId]; if(node) { [RCTConvert setNodeProperties:node properties:properties]; + if(node.geometry && properties[@"shape"]) { + [RCTConvert setShapeProperties:node.geometry properties:properties[@"shape"]]; + } + if(properties[@"material"]) { + for (id material in node.geometry.materials) { + [RCTConvert setMaterialProperties:material properties:properties[@"material"]]; + } + } + if(node.light) { + [RCTConvert setLightProperties:node.light properties:properties]; + } + + } } + + #pragma mark - RCTARKitSessionDelegate - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame { simd_float4 pos = frame.camera.transform.columns[3]; @@ -221,4 +270,21 @@ - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame { } +- (float)getCameraDistanceToPoint:(SCNVector3)point { + return getDistance(self.cameraOrigin.position, point); +} + +static float getDistance(const SCNVector3 pointA, const SCNVector3 pointB) { + float xd = pointB.x - pointA.x; + float yd = pointB.y - pointA.y; + float zd = pointB.z - pointA.z; + float distance = sqrt(xd * xd + yd * yd + zd * zd); + + if (distance < 0){ + return (distance * -1); + } else { + return (distance); + } +} + @end diff --git a/ios/RCTConvert+ARKit.h b/ios/RCTConvert+ARKit.h index 0576ed06..05cb6063 100644 --- a/ios/RCTConvert+ARKit.h +++ b/ios/RCTConvert+ARKit.h @@ -31,11 +31,15 @@ + (SCNTorus *)SCNTorus:(id)json; + (SCNCapsule *)SCNCapsule:(id)json; + (SCNPlane *)SCNPlane:(id)json; ++ (SCNShape * )SCNShape:(id)json; ++ (SCNLight *)SCNLight:(id)json; + (SCNTextNode *)SCNTextNode:(id)json; + (void)setNodeProperties:(SCNNode *)node properties:(id)json; + (void)setMaterialProperties:(SCNMaterial *)material properties:(id)json; ++ (void)setShapeProperties:(SCNGeometry *)geometry properties:(id)json; ++ (void)setLightProperties:(SCNLight *)light properties:(id)json; @end diff --git a/ios/RCTConvert+ARKit.m b/ios/RCTConvert+ARKit.m index 91a22c24..5e27d006 100644 --- a/ios/RCTConvert+ARKit.m +++ b/ios/RCTConvert+ARKit.m @@ -7,6 +7,7 @@ // #import "RCTConvert+ARKit.h" +#import "SVGBezierPath.h" @implementation RCTConvert (ARKit) @@ -37,35 +38,46 @@ + (SCNVector4)SCNVector4:(id)json { + (SCNNode *)SCNNode:(id)json { SCNNode *node = [SCNNode new]; - + node.name = [NSString stringWithFormat:@"%@", json[@"id"]]; [self setNodeProperties:node properties:json]; - + return node; } + ++ (void)addMaterials:(SCNGeometry *)geometry json:(id)json sides:(int) sides { + SCNMaterial *material = [self SCNMaterial:json[@"material"]]; + + NSMutableArray *materials = [NSMutableArray array]; + for (int i = 0; i < sides; i++) + [materials addObject: material]; + geometry.materials = materials; +} + + (SCNBox *)SCNBox:(id)json { NSDictionary *shape = json[@"shape"]; + + CGFloat width = [shape[@"width"] floatValue]; CGFloat height = [shape[@"height"] floatValue]; CGFloat length = [shape[@"length"] floatValue]; CGFloat chamfer = [shape[@"chamfer"] floatValue]; SCNBox *geometry = [SCNBox boxWithWidth:width height:height length:length chamferRadius:chamfer]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material, material, material, material, material, material]; - + [self addMaterials:geometry json:json sides:6]; + return geometry; } + + (SCNSphere *)SCNSphere:(id)json { NSDictionary* shape = json[@"shape"]; CGFloat radius = [shape[@"radius"] floatValue]; SCNSphere *geometry = [SCNSphere sphereWithRadius:radius]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material]; - + [self addMaterials:geometry json:json sides:1]; + return geometry; } @@ -75,8 +87,7 @@ + (SCNCylinder *)SCNCylinder:(id)json { CGFloat height = [shape[@"height"] floatValue]; SCNCylinder *geometry = [SCNCylinder cylinderWithRadius:radius height:height]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material, material, material]; + [self addMaterials:geometry json:json sides:3]; return geometry; } @@ -88,8 +99,7 @@ + (SCNCone *)SCNCone:(id)json { CGFloat height = [shape[@"height"] floatValue]; SCNCone *geometry = [SCNCone coneWithTopRadius:topR bottomRadius:bottomR height:height]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material, material]; + [self addMaterials:geometry json:json sides:2]; return geometry; } @@ -101,8 +111,7 @@ + (SCNPyramid *)SCNPyramid:(id)json { CGFloat height = [shape[@"height"] floatValue]; SCNPyramid *geometry = [SCNPyramid pyramidWithWidth:width height:height length:length]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material, material, material, material, material]; + [self addMaterials:geometry json:json sides:5]; return geometry; } @@ -114,8 +123,7 @@ + (SCNTube *)SCNTube:(id)json { CGFloat height = [shape[@"height"] floatValue]; SCNTube *geometry = [SCNTube tubeWithInnerRadius:innerR outerRadius:outerR height:height]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material, material, material, material]; + [self addMaterials:geometry json:json sides:4]; return geometry; } @@ -126,20 +134,18 @@ + (SCNTorus *)SCNTorus:(id)json { CGFloat pipeR = [shape[@"pipeR"] floatValue]; SCNTorus *geometry = [SCNTorus torusWithRingRadius:ringR pipeRadius:pipeR]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material]; + [self addMaterials:geometry json:json sides:1]; return geometry; } - + + (SCNCapsule *)SCNCapsule:(id)json { NSDictionary* shape = json[@"shape"]; CGFloat capR = [shape[@"capR"] floatValue]; CGFloat height = [shape[@"height"] floatValue]; SCNCapsule *geometry = [SCNCapsule capsuleWithCapRadius:capR height:height]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - geometry.materials = @[material]; + [self addMaterials:geometry json:json sides:1]; return geometry; } @@ -161,9 +167,75 @@ + (SCNPlane *)SCNPlane:(id)json { if(shape[@"heightSegmentCount"]) { geometry.heightSegmentCount = [shape[@"heightSegmentCount"] intValue]; } - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - material.doubleSided = YES; - geometry.materials = @[material]; + [self addMaterials:geometry json:json sides:1]; + + return geometry; +} + ++ (SVGBezierPath *)svgStringToBezier:(NSString *)pathString { + NSArray * paths = [SVGBezierPath pathsFromSVGString:pathString]; + SVGBezierPath * fullPath; + for(SVGBezierPath *path in paths) { + if(!fullPath) { + fullPath = path; + } else { + [fullPath appendPath:path]; + } + } + return fullPath; +} + ++ (void)setChamferProfilePathSvg:(SCNShape *)geometry properties:(NSDictionary *)shape { + if (shape[@"chamferProfilePathSvg"]) { + + + SVGBezierPath * path = [self svgStringToBezier:shape[@"chamferProfilePathSvg"]]; + if(shape[@"chamferProfilePathFlatness"]) { + path.flatness = [shape[@"chamferProfilePathFlatness"] floatValue]; + } + // normalize path + CGRect boundingBox = path.bounds; + if(path.bounds.size.width !=0 && path.bounds.size.height != 0) { + CGFloat scaleX = 1/boundingBox.size.width; + CGFloat scaleY = scaleY = 1/boundingBox.size.height; + + CGAffineTransform transform = CGAffineTransformMakeScale(scaleX, scaleY); + [path applyTransform:transform]; + geometry.chamferProfile = path; + } else { + NSLog(@"Invalid chamferProfilePathFlatness"); + } + } + +} + ++ (SCNShape * )SCNShape:(id)json { + NSDictionary* shape = json[@"shape"]; + + + NSString * pathString = shape[@"pathSvg"]; + + SVGBezierPath * path = [self svgStringToBezier:pathString]; + + if (shape[@"pathFlatness"]) { + path.flatness = [shape[@"pathFlatness"] floatValue]; + } + + CGFloat extrusion = [shape[@"extrusion"] floatValue]; + SCNShape *geometry = [SCNShape shapeWithPath:path extrusionDepth:extrusion]; + + if (shape[@"chamferMode"]) { + geometry.chamferMode = (SCNChamferMode) [shape[@"chamferMode"] integerValue]; + } + if (shape[@"chamferRadius"]) { + geometry.chamferRadius = [shape[@"chamferRadius"] floatValue]; + } + if (shape[@"chamferProfilePathSvg"]) { + [self setChamferProfilePathSvg:geometry properties:shape]; + } + + [self addMaterials:geometry json:json sides:1]; + return geometry; } @@ -183,7 +255,7 @@ + (SCNTextNode *)SCNTextNode:(id)json { CGFloat fontSize = [font[@"size"] floatValue]; CGFloat size = fontSize / 12; SCNText *scnText = [SCNText textWithString:text extrusionDepth:depth / size]; - + scnText.flatness = 0.1; // font @@ -203,19 +275,21 @@ + (SCNTextNode *)SCNTextNode:(id)json { // material // scnText.materials = @[face, face, border, border, border]; - SCNMaterial *material = [self SCNMaterial:json[@"material"]]; - scnText.materials = @[material, material, material, material, material]; + [self addMaterials:scnText json:json sides:5]; // SCNTextNode SCNTextNode *textNode = [SCNNode nodeWithGeometry:scnText]; + textNode.name = [NSString stringWithFormat:@"%@", json[@"id"]]; + + textNode.scale = SCNVector3Make(size, size, size); // position textNode SCNVector3 min = SCNVector3Zero; SCNVector3 max = SCNVector3Zero; [textNode getBoundingBoxMin:&min max:&max]; - + textNode.position = SCNVector3Make(-(min.x + max.x) / 2 * size, -(min.y + max.y) / 2 * size, -(min.z + max.z) / 2 * size); @@ -224,21 +298,64 @@ + (SCNTextNode *)SCNTextNode:(id)json { } ++ (SCNLight *)SCNLight:(id)json { + SCNLight * light = [SCNLight light]; + [self setLightProperties:light properties:json]; + return light; +} + + ++ (void)setMaterialPropertyContents:(id)property material:(SCNMaterialProperty *)material { + if (property[@"path"]) { + material.contents = property[@"path"]; + } else if (property[@"color"]) { + material.contents = [self UIColor:property[@"color"]]; + } + if (property[@"intensity"]) { + material.intensity = [property[@"intensity"] floatValue]; + } +} + + (void)setMaterialProperties:(SCNMaterial *)material properties:(id)json { + if (json[@"doubleSided"]) { + material.doubleSided = [json[@"doubleSided"] boolValue]; + } else { + material.doubleSided = YES; + } + if (json[@"blendMode"]) { material.blendMode = (SCNBlendMode) [json[@"blendMode"] integerValue]; } + if (json[@"lightingModel"]) { material.lightingModelName = json[@"lightingModel"]; } + if (json[@"diffuse"]) { - material.diffuse.contents = [self UIColor:json[@"diffuse"]]; + [self setMaterialPropertyContents:json[@"diffuse"] material:material.diffuse]; + } + + if (json[@"normal"]) { + [self setMaterialPropertyContents:json[@"normal"] material:material.normal]; + } + + if (json[@"displacement"]) { + [self setMaterialPropertyContents:json[@"displacement"] material:material.displacement]; + } + + if (json[@"specular"]) { + [self setMaterialPropertyContents:json[@"specular"] material:material.specular]; + } + + if (json[@"transparency"]) { + material.transparency = [json[@"transparency"] floatValue]; } if (json[@"metalness"]) { material.lightingModelName = SCNLightingModelPhysicallyBased; material.metalness.contents = @([json[@"metalness"] floatValue]); } + if (json[@"roughness"]) { material.lightingModelName = SCNLightingModelPhysicallyBased; material.roughness.contents = @([json[@"roughness"] floatValue]); @@ -247,9 +364,39 @@ + (void)setMaterialProperties:(SCNMaterial *)material properties:(id)json { if(json[@"shaders"] ) { material.shaderModifiers = json[@"shaders"]; } + + if(json[@"writesToDepthBuffer"] ) { + material.writesToDepthBuffer = [json[@"writesToDepthBuffer"] boolValue]; + } + + if(json[@"colorBufferWriteMask"] ) { + material.colorBufferWriteMask = [json[@"colorBufferWriteMask"] integerValue]; + } + + if(json[@"fillMode"] ) { + material.fillMode = [json[@"fillMode"] integerValue]; + } + + if(json[@"doubleSided"]) { + material.doubleSided = [json[@"doubleSided"] boolValue]; + } + + if(json[@"litPerPixel"]) { + material.litPerPixel = [json[@"litPerPixel"] boolValue]; + } } + (void)setNodeProperties:(SCNNode *)node properties:(id)json { + + if (json[@"categoryBitMask"]) { + node.categoryBitMask = [json[@"categoryBitMask"] integerValue]; + } + if (json[@"renderingOrder"]) { + node.renderingOrder = [json[@"renderingOrder"] integerValue]; + } + if (json[@"castsShadow"]) { + node.castsShadow = [json[@"castsShadow"] boolValue]; + } if(json[@"transition"]) { NSDictionary * transition =json[@"transition"]; if(transition[@"duration"]) { @@ -257,7 +404,7 @@ + (void)setNodeProperties:(SCNNode *)node properties:(id)json { } else { [SCNTransaction setAnimationDuration:0.0]; } - + } else { [SCNTransaction setAnimationDuration:0.0]; } @@ -265,6 +412,12 @@ + (void)setNodeProperties:(SCNNode *)node properties:(id)json { node.position = [self SCNVector3:json[@"position"]]; } + if (json[@"scale"]) { + CGFloat scale = [json[@"scale"] floatValue]; + node.scale = SCNVector3Make(scale, scale, scale); + + } + if (json[@"eulerAngles"]) { node.eulerAngles = [self SCNVector3:json[@"eulerAngles"]]; } @@ -276,8 +429,118 @@ + (void)setNodeProperties:(SCNNode *)node properties:(id)json { if (json[@"rotation"]) { node.rotation = [self SCNVector4:json[@"rotation"]]; } + + if (json[@"opacity"]) { + node.opacity = [json[@"opacity"] floatValue]; + } } ++ (NSSet *) specialShapeProperties { + return [[NSSet alloc] initWithArray: + @[@"pathSvg", @"chamferProfilePathSvg"]]; +} + + ++ (void)setShapeProperties:(SCNGeometry *)geometry properties:(id)shapeJson { + + // most properties are strings + for (NSString* key in shapeJson) { + if(![self.specialShapeProperties containsObject:key]) { + id value = [NSNumber numberWithFloat:[shapeJson[key] floatValue]]; + [geometry setValue:value forKey:key]; + } + } + + if([geometry isKindOfClass:[SCNShape class]]) { + SCNShape * shapeGeometry = (SCNShape * ) geometry; + if(shapeJson[@"pathSvg"]) { + NSString * pathString = shapeJson[@"pathSvg"]; + SVGBezierPath * path = [self svgStringToBezier:pathString]; + if (shapeJson[@"pathFlatness"]) { + path.flatness = [shapeJson[@"pathFlatness"] floatValue]; + } + shapeGeometry.path = path; + } + if (shapeJson[@"chamferProfilePathSvg"]) { + [self setChamferProfilePathSvg: shapeGeometry properties:shapeJson]; + } + + } +} + + + ++ (void)setLightProperties:(SCNLight *)light properties:(id)json { + if (json[@"lightCategoryBitMask"]) { + light.categoryBitMask = [json[@"lightCategoryBitMask"] integerValue]; + } + if(json[@"type"]) { + light.type = json[@"type"]; + } + if(json[@"color"]) { + light.color = (__bridge id _Nonnull)([RCTConvert CGColor:json[@"color"]]); + } + if(json[@"temperature"]) { + light.temperature = [json[@"temperature"] floatValue]; + } + + if(json[@"intensity"]) { + light.intensity = [json[@"intensity"] floatValue]; + } + + if(json[@"attenuationStartDistance"]) { + light.attenuationStartDistance = [json[@"attenuationStartDistance"] floatValue]; + } + + if(json[@"attenuationEndDistance"]) { + light.attenuationEndDistance = [json[@"attenuationEndDistance"] floatValue]; + } + + if(json[@"spotInnerAngle"]) { + light.spotInnerAngle = [json[@"spotInnerAngle"] floatValue]; + } + + if(json[@"spotOuterAngle"]) { + light.spotOuterAngle = [json[@"spotOuterAngle"] floatValue]; + } + + if(json[@"castsShadow"]) { + light.castsShadow = [json[@"castsShadow"] boolValue]; + } + + if(json[@"shadowRadius"]) { + light.shadowRadius = [json[@"shadowRadius"] floatValue]; + } + + if(json[@"shadowColor"]) { + light.shadowColor = (__bridge id _Nonnull)([RCTConvert CGColor:json[@"shadowColor"]]); + } + + + if(json[@"shadowSampleCount"]) { + light.shadowSampleCount = [json[@"shadowSampleCount"] integerValue]; + } + + if(json[@"shadowBias"]) { + light.shadowBias = [json[@"shadowBias"] floatValue]; + } + + if(json[@"shadowMode"]) { + light.shadowMode = [json[@"shadowMode"] integerValue]; + } + if(json[@"orthographicScale"]) { + light.orthographicScale = [json[@"orthographicScale"] floatValue]; + } + + if(json[@"zFar"]) { + light.zFar = [json[@"zFar"] floatValue]; + } + + if(json[@"zNear"]) { + light.zNear = [json[@"zNear"] floatValue]; + } +} + @end diff --git a/ios/color-grabber/color-grabber.h b/ios/color-grabber/color-grabber.h new file mode 100644 index 00000000..69cb1eaf --- /dev/null +++ b/ios/color-grabber/color-grabber.h @@ -0,0 +1,13 @@ +// +// color-grabber.h +// + +#import +#import +#import + +@interface ColorGrabber : NSObject + ++ (instancetype)sharedInstance; +- (NSArray *)getColorsFromImage:(UIImage *)image options:(NSDictionary *)options; +@end diff --git a/ios/color-grabber/color-grabber.m b/ios/color-grabber/color-grabber.m new file mode 100644 index 00000000..34b06fc9 --- /dev/null +++ b/ios/color-grabber/color-grabber.m @@ -0,0 +1,250 @@ + +#import +#import +#import "color-grabber.h" + +#define CLAMP(x, low, high) ({\ +__typeof__(x) __x = (x); \ +__typeof__(low) __low = (low);\ +__typeof__(high) __high = (high);\ +__x > __high ? __high : (__x < __low ? __low : __x);\ +}) + +@implementation ColorGrabber + + + + +RCT_EXPORT_MODULE(); + +- (NSArray *)getColorsFromImage:(UIImage *)image options:(NSDictionary *)options { + + float dimension = [RCTConvert float:options[@"dimension"]]; // 4 + float flexibility = [RCTConvert float:options[@"flexibility"]]; // 5; + float range = [RCTConvert float:options[@"range"]]; // 40; + + NSMutableArray * colours = [NSMutableArray new]; + CGImageRef imageRef = [image CGImage]; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + unsigned char *rawData = (unsigned char*) calloc(dimension * dimension * 4, sizeof(unsigned char)); + NSUInteger bytesPerPixel = 4; + NSUInteger bytesPerRow = bytesPerPixel * dimension; + NSUInteger bitsPerComponent = 8; + CGContextRef context = CGBitmapContextCreate(rawData, dimension, dimension, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGColorSpaceRelease(colorSpace); + CGContextDrawImage(context, CGRectMake(0, 0, dimension, dimension), imageRef); + CGContextRelease(context); + + float x = 0; + float y = 0; + for (int n = 0; n<(dimension*dimension); n++){ + + int index = (bytesPerRow * y) + x * bytesPerPixel; + int red = rawData[index]; + int green = rawData[index + 1]; + int blue = rawData[index + 2]; + int alpha = rawData[index + 3]; + NSArray * a = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%i",red],[NSString stringWithFormat:@"%i",green],[NSString stringWithFormat:@"%i",blue],[NSString stringWithFormat:@"%i",alpha], nil]; + [colours addObject:a]; + + y++; + if (y==dimension){ + y=0; + x++; + } + } + free(rawData); + + // Add some colour flexibility (adds more colours either side of the colours in the image) + NSArray * copyColours = [NSArray arrayWithArray:colours]; + NSMutableArray * flexibleColours = [NSMutableArray new]; + + float flexFactor = flexibility * 2 + 1; + float factor = flexFactor * flexFactor * 3; //(r,g,b) == *3 + for (int n = 0; n<(dimension * dimension); n++){ + + NSArray * pixelColours = copyColours[n]; + NSMutableArray * reds = [NSMutableArray new]; + NSMutableArray * greens = [NSMutableArray new]; + NSMutableArray * blues = [NSMutableArray new]; + + for (int p = 0; p<3; p++){ + + NSString * rgbStr = pixelColours[p]; + int rgb = [rgbStr intValue]; + + for (int f = -flexibility; f= ranged_r-range && r<= ranged_r+range){ + if (g>= ranged_g-range && g<= ranged_g+range){ + if (b>= ranged_b-range && b<= ranged_b+range){ + exclude = true; + } + } + } + } + + if (!exclude){ [ranges addObject:key]; } + } + + + + // If you want percentages to colours continue below + NSMutableDictionary * temp = [NSMutableDictionary new]; + float totalCount = 0.0f; + for (NSString * rangeKey in ranges){ + NSNumber * count = colourCounter[rangeKey]; + totalCount += [count intValue]; + temp[rangeKey]=count; + } + + // Set percentages + NSMutableArray * colors = [NSMutableArray new]; + for (NSString * key in temp){ + float count = [temp[key] floatValue]; + float percentage = count/totalCount; + NSLog(@"%f",percentage); + NSArray * rgb = [key componentsSeparatedByString:@","]; + float r = [rgb[0] floatValue]; + float g = [rgb[1] floatValue]; + float b = [rgb[2] floatValue]; + + + + [colors addObject:@{ + @"color": @{ + @"r": @(r), + @"g": @(g), + @"b": @(b) + }, + @"percentage":@(percentage) + }]; + + } + return colors; +} + +RCT_EXPORT_METHOD(getColors:(NSString *)path options:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + + NSURL* aURL = [NSURL URLWithString:path]; + + if([path hasPrefix: @"/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:path]; + + NSArray * colors = [self getColorsFromImage:image options:options]; + resolve(colors); + } else { + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + + [library assetForURL:aURL resultBlock:^(ALAsset *asset) { + UIImage *image = [UIImage imageWithCGImage:[[asset defaultRepresentation] fullScreenImage] scale:0.5 orientation:UIImageOrientationUp]; + NSArray * colors = [self getColorsFromImage:image options:options]; + resolve(colors); + + } + failureBlock:^(NSError *error) { + reject(@"asset error", @"cant't get colors from asset", error ); + }]; + } + + + + +} + +- (NSString *)hexStringFromColor:(UIColor *)color { + const CGFloat *components = CGColorGetComponents(color.CGColor); + + CGFloat r = components[0]; + CGFloat g = components[1]; + CGFloat b = components[2]; + + return [NSString stringWithFormat:@"#%02lX%02lX%02lX", + lroundf(r * 255), + lroundf(g * 255), + lroundf(b * 255)]; +} + + ++ (instancetype)sharedInstance { + static ColorGrabber *instance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + if (instance == nil) { + instance = [[self alloc] init]; + } + }); + return instance; +} + + +@end + + + diff --git a/ios/components/ARGeosManager.m b/ios/components/ARGeosManager.m index 754cc4b8..c6a26757 100644 --- a/ios/components/ARGeosManager.m +++ b/ios/components/ARGeosManager.m @@ -18,6 +18,7 @@ @implementation ARGeosManager [[RCTARKitNodes sharedInstance] addNodeToScene:node inReferenceFrame:frame]; } + RCT_EXPORT_METHOD(addSphere:(SCNSphere *)geometry node:(SCNNode *)node frame:(NSString *)frame) { node.geometry = geometry; [[RCTARKitNodes sharedInstance] addNodeToScene:node inReferenceFrame:frame]; @@ -58,11 +59,22 @@ @implementation ARGeosManager [[RCTARKitNodes sharedInstance] addNodeToScene:node inReferenceFrame:frame]; } +RCT_EXPORT_METHOD(addShape:(SCNShape *)geometry node:(SCNNode *)node frame:(NSString *)frame) { + node.geometry = geometry; + [[RCTARKitNodes sharedInstance] addNodeToScene:node inReferenceFrame:frame]; +} + +RCT_EXPORT_METHOD(addLight:(SCNLight *)light node:(SCNNode *)node frame:(NSString *)frame) { + node.light = light; + [[RCTARKitNodes sharedInstance] addNodeToScene:node inReferenceFrame:frame]; +} + + RCT_EXPORT_METHOD(unmount:(NSString *)identifier) { [[RCTARKitNodes sharedInstance] removeNodeForKey:identifier]; } -RCT_EXPORT_METHOD(update:(NSString *)identifier properties:(NSDictionary *) properties) { +RCT_EXPORT_METHOD(updateNode:(NSString *)identifier properties:(NSDictionary *) properties) { [[RCTARKitNodes sharedInstance] updateNode:identifier properties:properties]; } diff --git a/lib/colorUtils.js b/lib/colorUtils.js new file mode 100644 index 00000000..d41e81e9 --- /dev/null +++ b/lib/colorUtils.js @@ -0,0 +1,43 @@ +// kudos to to https://github.com/jverhoelen/camanjs-whitebalance/blob/master/src/caman.whitebalance.js +import { NativeModules } from 'react-native'; + +const ARKitManager = NativeModules.ARKitManager; + +export const colorTemperatureToRgb = temperature => { + const m = global.Math; + const temp = temperature / 100; + let r; + let g; + let b; + + if (temp <= 66) { + r = 255; + g = m.min(m.max(99.4708025861 * m.log(temp) - 161.1195681661, 0), 255); + } else { + r = m.min(m.max(329.698727446 * m.pow(temp - 60, -0.1332047592), 0), 255); + g = m.min(m.max(288.1221695283 * m.pow(temp - 60, -0.0755148492), 0), 255); + } + + if (temp >= 66) { + b = 255; + } else if (temp <= 19) { + b = 0; + } else { + b = temp - 10; + b = m.min(m.max(138.5177312231 * m.log(b) - 305.0447927307, 0), 255); + } + + return { + r, + g, + b, + }; +}; +export const whiteBalanceWithTemperature = ({ r, g, b }, temperature) => { + const temperatureRgb = colorTemperatureToRgb(temperature); + return { + r: r * 255 / temperatureRgb.r, + g: g * 255 / temperatureRgb.g, + b: b * 255 / temperatureRgb.b, + }; +}; diff --git a/lib/pickColors.js b/lib/pickColors.js new file mode 100644 index 00000000..2d83a1fc --- /dev/null +++ b/lib/pickColors.js @@ -0,0 +1,65 @@ +import { NativeModules } from 'react-native'; + +import { whiteBalanceWithTemperature } from './colorUtils'; + +const ARKitManager = NativeModules.ARKitManager; + +const doWhiteBalance = async (colors, { includeRawColors }) => { + const lightEstimation = await ARKitManager.getCurrentLightEstimation(); + + if (!lightEstimation) { + return colors; + } + + return colors.map(({ color, ...p }) => ({ + color: whiteBalanceWithTemperature( + color, + lightEstimation.ambientColorTemperature, + ), + ...p, + ...(includeRawColors ? { colorRaw: color } : {}), + })); +}; +export const pickColorsFromFile = async ( + filePath, + { + whiteBalance = true, + includeRawColors = false, + // color grabber options, currently undocumented + range = 40, + dimension = 4, + flexibility = 5, + } = {}, +) => { + const colors = await ARKitManager.pickColorsRawFromFile(filePath, { + range, + dimension, + flexibility, + }); + if (!whiteBalance) { + return colors; + } + return doWhiteBalance(colors, { includeRawColors }); +}; +export const pickColors = async ( + { + whiteBalance = true, + includeRawColors = false, + selection = null, + // color grabber options, currently undocumented + range = 40, + dimension = 4, + flexibility = 5, + } = {}, +) => { + const colors = await ARKitManager.pickColorsRaw({ + selection, + range, + dimension, + flexibility, + }); + if (!whiteBalance) { + return colors; + } + return doWhiteBalance(colors, { includeRawColors }); +}; diff --git a/package.json b/package.json index a9bb2cf5..6d322f1b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "https://github.com/HippoAR/react-native-arkit.git" }, - "version": "0.3.0", + "version": "0.6.1-beta.1", "description": "React Native binding for iOS ARKit", "author": "Zehao Li ", "license": "MIT", @@ -14,12 +14,17 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, + "peerDependencies": { + "react-native": ">=0.46.0", + "react": "*", + "prop-types": "*" + }, "dependencies": { + "@panter/react-animation-frame": "^0.3.7", + "_PocketSVG": "https://github.com/pocketsvg/PocketSVG", "fast-deep-equal": "^1.0.0", "lodash": "^4.17.4", - "prop-types": "^15.5.7", - "react": "16.0.0-alpha.12", - "react-animation-frame": "^0.3.5" + "prop-types": "^15.5.7" }, "devDependencies": { "babel-eslint": "^7.2.3", @@ -32,6 +37,8 @@ "eslint-plugin-jsx-a11y": "^5.0.3", "eslint-plugin-prettier": "^2.1.1", "eslint-plugin-react": "^7.0.1", - "prettier": "^1.3.1" + "prettier": "^1.3.1", + "react": "16.0.0-alpha.12", + "prop-types": "^15.5.8" } } diff --git a/startup.js b/startup.js new file mode 100644 index 00000000..4899c34b --- /dev/null +++ b/startup.js @@ -0,0 +1,10 @@ +import { NativeModules } from 'react-native'; + +const ARKitManager = NativeModules.ARKitManager; + +export default () => { + // when reloading the app, the scene should be cleared. + // on prod, this usually does not happen, but you can reload the app in develop mode + // without clearing, this would result in inconsistency + ARKitManager.clearScene(); +}; diff --git a/yarn.lock b/yarn.lock index 3655f102..c0b0f12a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@panter/react-animation-frame@^0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@panter/react-animation-frame/-/react-animation-frame-0.3.7.tgz#31a0d7683e4a8d2e1b29ff512cad36513bd43d00" + dependencies: + react "15.4.2" + +"_PocketSVG@https://github.com/pocketsvg/PocketSVG": + version "0.0.0" + resolved "https://github.com/pocketsvg/PocketSVG#e6d84ddeb11f99c4420e354ebb9fd34db2cf507a" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -1417,12 +1427,6 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -react-animation-frame@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/react-animation-frame/-/react-animation-frame-0.3.5.tgz#6afe77e8c8b8774c56183598ba40c6de4dce4588" - dependencies: - react "15.4.2" - react-deep-force-update@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz#bcd31478027b64b3339f108921ab520b4313dc2c"