phetsims / buoyancy

"Buoyancy" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
2 stars 2 forks source link

Add a duck object to the shapes screen? #101

Closed zepumph closed 4 days ago

zepumph commented 2 months ago

From the design meeting today, we humored the idea of having an extra, irregular shape on the shapes screen. A duck seems like an obvious choice, but we will want to do some investigation to see how hard it would be. @AgustinVallejo mentioned that he knows enough about blender that it isn't automatically out of the question to use our own custom mesh for this.

AgustinVallejo commented 1 month ago

This was quite a challenge, and I will describe my findings in this comment. And afterwards, the preliminary patch of these changes.

  1. @jonathanolson suggested we look into the implementation of LonePairGeometryData.js which in a way imports geometry data from a file called balloon2.obj (multiple files but that's not important).
  2. That file was converted into a JSON object via a python script from Three.js release 85. I tried to use it but it's in Python 2 and I didn't think it was worth it to downgrade my python version for this.
  3. In three.js-r86 they deprecated that python script and replaced it for the obj2three.js node script. That one was also eventually deprecated and the current way of doing this is quite different, but by downloading r86 and running that script we got it to work. Not great, but not terrible. (commit above updates balloon2-README.txt and links to this issue)
  4. From then on it was a matter of adding the json into DuckData and playing around with DuckView. We're not including duck files in this patch since they are quite heavy and we want to settle for a definite shape before uploading unnecessarily.

The following patch sloppily implements this, without the actual duck geometry. We still have to figure out how to get the physics body vertices working, as well as the transverse area (projection?). @zepumph and I are on this.

```diff Subject: [PATCH] Initial Duck go --- Index: js/buoyancy/view/DuckView.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/view/DuckView.ts b/js/buoyancy/view/DuckView.ts new file mode 100644 --- /dev/null (date 1710963085166) +++ b/js/buoyancy/view/DuckView.ts (date 1710963085166) @@ -0,0 +1,45 @@ +// Copyright 2019-2024, University of Colorado Boulder + +/** + * The 3D view for a Duck. + * + * @author Agustín Vallejo + * @author Michael Kauzman + */ + +import Bounds3 from '../../../../dot/js/Bounds3.js'; +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; +import MassView, { ModelPoint3ToViewPoint2 } from '../../common/view/MassView.js'; +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; +import Duck, { mainDuckGeometry } from '../model/Duck.js'; + +export default class DuckView extends MassView { + + public readonly duck: Duck; + private readonly duckGeometry: THREE.SphereGeometry; + + // private readonly updateListener: ( newSize: Bounds3, oldSize: Bounds3 ) => void; + + public constructor( duck: Duck, modelToViewPoint: ModelPoint3ToViewPoint2, dragBoundsProperty: TReadOnlyProperty ) { + + const duckGeometry = mainDuckGeometry.children[ 0 ].geometry; + + super( duck, duckGeometry, modelToViewPoint, dragBoundsProperty ); + + this.duck = duck; + this.duckGeometry = duckGeometry; + this.duckGeometry.scale( 0.1, 0.1, 0.1 ); + } + + /** + * Releases references. + */ + public override dispose(): void { + this.duck.sizeProperty.unlink( this.updateListener ); + this.duckGeometry.dispose(); + + super.dispose(); + } +} + +densityBuoyancyCommon.register( 'DuckView', DuckView ); Index: js/common/data/DuckData.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/data/DuckData.ts b/js/common/data/DuckData.ts new file mode 100644 --- /dev/null (date 1710963127371) +++ b/js/common/data/DuckData.ts (date 1710963127371) @@ -0,0 +1,15 @@ +// Copyright 2024, University of Colorado Boulder +/** + * DuckData modeled in blender by PhET, exported to .obj, and converted to THREE.js data using + * The obj2three.js script in three.js-r86/utils/converters/obj2three.js + * + * @author Agustín Vallejo (PhET Interactive Simulations) + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; + +const DuckData = {}; // eslint-disable-line + +densityBuoyancyCommon.register( 'DuckData', DuckData ); +export default DuckData; \ No newline at end of file Index: js/buoyancy/model/BuoyancyShapesModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/BuoyancyShapesModel.ts b/js/buoyancy/model/BuoyancyShapesModel.ts --- a/js/buoyancy/model/BuoyancyShapesModel.ts (revision 9f2a384d8fd424d7cd0d7e04019225bd21a32ec6) +++ b/js/buoyancy/model/BuoyancyShapesModel.ts (date 1710881257946) @@ -30,6 +30,7 @@ import TProperty from '../../../../axon/js/TProperty.js'; import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; import MassTag from '../../common/model/MassTag.js'; +import Duck from './Duck.js'; export type BuoyancyShapesModelOptions = DensityBuoyancyModelOptions; @@ -82,7 +83,7 @@ } ); this.availableMasses.push( this.scale1 ); - this.primaryShapeProperty = new EnumerationProperty( MassShape.BLOCK, { + this.primaryShapeProperty = new EnumerationProperty( MassShape.DUCK, { tandem: tandem.createTandem( 'primaryShapeProperty' ) } ); this.secondaryShapeProperty = new EnumerationProperty( MassShape.INVERTED_CONE, { @@ -143,6 +144,9 @@ false, massOptions ); + break; + case MassShape.DUCK: + mass = new Duck( this.engine, Duck.getSizeFromRatios( widthRatio, heightRatio ), massOptions ); break; default: throw new Error( `shape not recognized: ${shape}` ); Index: js/common/view/DensityBuoyancyScreenView.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/view/DensityBuoyancyScreenView.ts b/js/common/view/DensityBuoyancyScreenView.ts --- a/js/common/view/DensityBuoyancyScreenView.ts (revision 9f2a384d8fd424d7cd0d7e04019225bd21a32ec6) +++ b/js/common/view/DensityBuoyancyScreenView.ts (date 1710882121252) @@ -66,6 +66,8 @@ import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js'; import grabSoundPlayer from '../../../../tambo/js/shared-sound-players/grabSoundPlayer.js'; import releaseSoundPlayer from '../../../../tambo/js/shared-sound-players/releaseSoundPlayer.js'; +import DuckView from '../../buoyancy/view/DuckView.js'; +import Duck from '../../buoyancy/model/Duck.js'; // constants const MARGIN = DensityBuoyancyCommonConstants.MARGIN; @@ -643,6 +645,9 @@ else if ( mass instanceof Boat ) { massView = new BoatView( mass, boundModelToViewPoint, dragBoundsProperty, model.pool.liquidYInterpolatedProperty ); } + else if ( mass instanceof Duck ) { + massView = new DuckView( mass, boundModelToViewPoint, dragBoundsProperty ); + } if ( massView ) { this.sceneNode.stage.threeScene.add( massView ); Index: js/common/model/MassShape.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/model/MassShape.ts b/js/common/model/MassShape.ts --- a/js/common/model/MassShape.ts (revision 9f2a384d8fd424d7cd0d7e04019225bd21a32ec6) +++ b/js/common/model/MassShape.ts (date 1710794800371) @@ -17,6 +17,7 @@ public static readonly HORIZONTAL_CYLINDER = new MassShape(); public static readonly CONE = new MassShape(); public static readonly INVERTED_CONE = new MassShape(); + public static readonly DUCK = new MassShape(); public static readonly enumeration = new Enumeration( MassShape, { phetioDocumentation: 'Shape of the mass' Index: js/buoyancy/view/ShapeSizeControlNode.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/view/ShapeSizeControlNode.ts b/js/buoyancy/view/ShapeSizeControlNode.ts --- a/js/buoyancy/view/ShapeSizeControlNode.ts (revision 9f2a384d8fd424d7cd0d7e04019225bd21a32ec6) +++ b/js/buoyancy/view/ShapeSizeControlNode.ts (date 1710798129746) @@ -21,6 +21,7 @@ import { MassShape } from '../../common/model/MassShape.js'; import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; import DensityBuoyancyCommonStrings from '../../DensityBuoyancyCommonStrings.js'; +import StringProperty from '../../../../axon/js/StringProperty.js'; // constants const shapeStringMap = { @@ -29,7 +30,8 @@ [ MassShape.VERTICAL_CYLINDER.name ]: DensityBuoyancyCommonStrings.shape.verticalCylinderStringProperty, [ MassShape.HORIZONTAL_CYLINDER.name ]: DensityBuoyancyCommonStrings.shape.horizontalCylinderStringProperty, [ MassShape.CONE.name ]: DensityBuoyancyCommonStrings.shape.coneStringProperty, - [ MassShape.INVERTED_CONE.name ]: DensityBuoyancyCommonStrings.shape.invertedConeStringProperty + [ MassShape.INVERTED_CONE.name ]: DensityBuoyancyCommonStrings.shape.invertedConeStringProperty, + [ MassShape.DUCK.name ]: new StringProperty( 'Duck' ) }; // TODO: this should come from somewhere else, https://github.com/phetsims/buoyancy/issues/90 const tandemNameMap = { @@ -38,7 +40,8 @@ [ MassShape.VERTICAL_CYLINDER.name ]: 'verticalCylinder', [ MassShape.HORIZONTAL_CYLINDER.name ]: 'horizontalCylinder', [ MassShape.CONE.name ]: 'cone', - [ MassShape.INVERTED_CONE.name ]: 'invertedCone' + [ MassShape.INVERTED_CONE.name ]: 'invertedCone', + [ MassShape.DUCK.name ]: 'duck' }; type SelfOptions = { Index: js/buoyancy/model/Duck.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/Duck.ts b/js/buoyancy/model/Duck.ts new file mode 100644 --- /dev/null (date 1710963085207) +++ b/js/buoyancy/model/Duck.ts (date 1710963085207) @@ -0,0 +1,248 @@ +// Copyright 2019-2024, University of Colorado Boulder + +/** + * An adjustable Duck + * + * @author Agustín Vallejo (PhET Interactive Simulations) + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +import Property from '../../../../axon/js/Property.js'; +import StrictOmit from '../../../../phet-core/js/types/StrictOmit.js'; +import Bounds3 from '../../../../dot/js/Bounds3.js'; +import Ray3 from '../../../../dot/js/Ray3.js'; +import Utils from '../../../../dot/js/Utils.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Vector3 from '../../../../dot/js/Vector3.js'; +import { Shape } from '../../../../kite/js/imports.js'; +import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; +import IOType from '../../../../tandem/js/types/IOType.js'; +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; +import Mass, { InstrumentedMassOptions, MASS_MAX_SHAPES_DIMENSION, MASS_MIN_SHAPES_DIMENSION } from '../../common/model/Mass.js'; +import PhysicsEngine from '../../common/model/PhysicsEngine.js'; +import { MassShape } from '../../common/model/MassShape.js'; +import DuckData from '../../common/data/DuckData.js'; + +export type DuckOptions = StrictOmit; + +const loader = new THREE.ObjectLoader(); + +export const mainDuckGeometry = loader.parse( DuckData ); + +const VERTICES = _.chunk( mainDuckGeometry.children[0].geometry.getAttribute('position').array, 3).map( vert3 => { + return new Vector2( vert3[0], vert3[2] ); +} ); + +export default class Duck extends Mass { + + public readonly sizeProperty: Property; + + // Step information + public stepMaximumArea: number; + public stepMaximumVolume: number; + + public constructor( engine: PhysicsEngine, size: Bounds3, providedConfig: DuckOptions ) { + + const config = optionize()( { + body: engine.createFromVertices( Duck.getDuckVertices( size.width, size.height ), false ), + shape: Duck.getDuckShape( size.width, size.height ), + volume: Duck.getVolume( size ), + massShape: MassShape.DUCK, + + phetioType: Duck.DuckIO + }, providedConfig ); + + assert && assert( !config.canRotate ); + + super( engine, config as InstrumentedMassOptions ); + + this.sizeProperty = new Property( size, { + valueType: Bounds3, + tandem: config.tandem.createTandem( 'sizeProperty' ), + phetioValueType: Bounds3.Bounds3IO + } ); + + this.stepMaximumArea = 0; + this.stepMaximumVolume = 0; + + this.updateSize( size ); + } + + public override getLocalBounds(): Bounds3 { + return this.sizeProperty.value; + } + + /** + * Updates the size of the duck. + */ + public updateSize( size: Bounds3 ): void { + this.engine.updateFromVertices( this.body, Duck.getDuckVertices( size.width, size.height ), false ); + this.sizeProperty.value = size; + this.shapeProperty.value = Duck.getDuckShape( size.width, size.height ); + + this.volumeLock = true; + this.volumeProperty.value = Duck.getVolume( size ); + this.volumeLock = false; + + this.forceOffsetProperty.value = new Vector3( 0, 0, size.maxZ ); + this.massLabelOffsetProperty.value = new Vector3( 0, size.minY * 0.5, size.maxZ * 0.7 ); + } + + /** + * Returns the general size of the mass based on a general size scale. + */ + public static getSizeFromRatios( widthRatio: number, heightRatio: number ): Bounds3 { + const x = ( MASS_MIN_SHAPES_DIMENSION + widthRatio * ( MASS_MAX_SHAPES_DIMENSION - MASS_MIN_SHAPES_DIMENSION ) ) / 2; + const y = ( MASS_MIN_SHAPES_DIMENSION + heightRatio * ( MASS_MAX_SHAPES_DIMENSION - MASS_MIN_SHAPES_DIMENSION ) ) / 2; + return new Bounds3( -x, -y, -x, x, y, x ); + } + + /** + * Sets the general size of the mass based on a general size scale. + */ + public setRatios( widthRatio: number, heightRatio: number ): void { + this.updateSize( Duck.getSizeFromRatios( widthRatio, heightRatio ) ); + } + + /** + * Called after a engine-physics-model step once before doing other operations (like computing buoyant forces, + * displacement, etc.) so that it can set high-performance flags used for this purpose. + * + * Type-specific values are likely to be set, but this should set at least stepX/stepBottom/stepTop + */ + public override updateStepInformation(): void { + super.updateStepInformation(); + + const xOffset = this.stepMatrix.m02(); + const yOffset = this.stepMatrix.m12(); + + this.stepX = xOffset; + this.stepBottom = yOffset + this.sizeProperty.value.minY; + this.stepTop = yOffset + this.sizeProperty.value.maxY; + + const a = this.sizeProperty.value.width / 2; + const b = this.sizeProperty.value.height / 2; + const c = this.sizeProperty.value.depth / 2; + this.stepMaximumArea = 4 * Math.PI * a * c; // 4 * pi * a * c + this.stepMaximumVolume = this.stepMaximumArea * b / 3; // 4/3 * pi * a * b * c + } + + /** + * If there is an intersection with the ray and this mass, the t-value (distance the ray would need to travel to + * reach the intersection, e.g. ray.position + ray.distance * t === intersectionPoint) will be returned. Otherwise + * if there is no intersection, null will be returned. + */ + public override intersect( ray: Ray3, isTouch: boolean ): number | null { + const translation = this.matrix.getTranslation().toVector3(); + const size = this.sizeProperty.value; + const relativePosition = ray.position.minusXYZ( translation.x, translation.y, translation.z ); + + const xp = 4 / ( size.width * size.width ); + const yp = 4 / ( size.height * size.height ); + const zp = 4 / ( size.depth * size.depth ); + + const a = xp * ray.direction.x * ray.direction.x + yp * ray.direction.y * ray.direction.y + zp * ray.direction.z * ray.direction.z; + const b = 2 * ( xp * relativePosition.x * ray.direction.x + yp * relativePosition.y * ray.direction.y + zp * relativePosition.z * ray.direction.z ); + const c = -1 + xp * relativePosition.x * relativePosition.x + yp * relativePosition.y * relativePosition.y + zp * relativePosition.z * relativePosition.z; + + const tValues = Utils.solveQuadraticRootsReal( a, b, c )!.filter( t => t > 0 ); + + if ( tValues.length ) { + return tValues[ 0 ]; + } + else { + return null; + } + } + + /** + * Returns the cumulative displaced volume of this object up to a given y level. + * + * Assumes step information was updated. + */ + public getDisplacedArea( liquidLevel: number ): number { + if ( liquidLevel < this.stepBottom || liquidLevel > this.stepTop ) { + return 0; + } + else { + const ratio = ( liquidLevel - this.stepBottom ) / ( this.stepTop - this.stepBottom ); + + return 0.1; // 4 * pi * a * c * ( t - t^2 ) + } + } + + /** + * Returns the displaced volume of this object up to a given y level, assuming a y value for the given liquid level. + * + * Assumes step information was updated. + */ + public getDisplacedVolume( liquidLevel: number ): number { + if ( liquidLevel <= this.stepBottom ) { + return 0; + } + else if ( liquidLevel >= this.stepTop ) { + return this.stepMaximumVolume; + } + else { + const ratio = ( liquidLevel - this.stepBottom ) / ( this.stepTop - this.stepBottom ); + + return this.stepMaximumVolume * ratio * ratio * ( 3 - 2 * ratio ); // 4/3 * pi * a * b * c * t^2 * ( 3 - 2t ) + } + } + + /** + * Resets things to their original values. + */ + public override reset(): void { + this.sizeProperty.reset(); + this.updateSize( this.sizeProperty.value ); + + super.reset(); + } + + /** + * Releases references + */ + public override dispose(): void { + this.sizeProperty.dispose(); + + super.dispose(); + } + + /** + * Returns a duck shape + */ + public static getDuckShape( width: number, height: number ): Shape { + return Shape.circle( Vector2.ZERO, height / 2 ); + } + + /** + * Returns vertices for a duck + */ + public static getDuckVertices( width: number, height: number ): Vector2[] { + + // a square + const vertices = [ + new Vector2( -width / 2, -height / 2 ), + new Vector2( width / 2, -height / 2 ), + new Vector2( width / 2, height / 2 ), + new Vector2( -width / 2, height / 2 ) + ]; + return vertices; + } + + /** + * Returns the volume of a duck with the given axis-aligned bounding box. + */ + public static getVolume( size: Bounds3 ): number { + return Math.PI * size.width * size.height * size.depth / 6; + } + + public static DuckIO = new IOType( 'DuckIO', { + valueType: Duck, + supertype: Mass.MassIO, + documentation: 'Represents a duck' + } ); +} + +densityBuoyancyCommon.register( 'Duck', Duck );
AgustinVallejo commented 1 month ago

duck3.json

Latest duck file :) (164KB)

AgustinVallejo commented 2 weeks ago

Update: Tried to test the new duck data but the patch was really outdated and had to merge new changes.

Here's a more up to date patch, but not entirely fixed as there are some assertions I couldn't get through:

```diff Subject: [PATCH] Updating Duck Patch --- Index: js/common/data/DuckData.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/data/DuckData.ts b/js/common/data/DuckData.ts new file mode 100644 --- /dev/null (date 1713526510869) +++ b/js/common/data/DuckData.ts (date 1713526510869) @@ -0,0 +1,15 @@ +// Copyright 2024, University of Colorado Boulder +/** + * DuckData modeled in blender by PhET, exported to .obj, and converted to THREE.js data using + * The obj2three.js script in three.js-r86/utils/converters/obj2three.js + * + * @author Agustín Vallejo (PhET Interactive Simulations) + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; + +const DuckData = {"metadata":{"version":4.5,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"DA0E4DEE-D2DB-4DBA-84B9-B4EE8A8AB258","type":"BufferGeometry","data":{"attributes":{"position":{"itemSize":3,"type":"Float32Array","array":[-0.866025,0.5,0,0,1,0,-0.866025,-0.5,0,-0.866025,0.5,0,0,-1,0,-0.866025,-0.5,0,0.866025,-0.5,0,0,-1,0,0.866025,0.5,0,0.866025,-0.5,0,0,1,0,0.866025,0.5,0],"normalized":false},"normal":{"itemSize":3,"type":"Float32Array","array":[0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1],"normalized":false}}}}],"materials":[{"uuid":"83E6A459-3112-4516-AB80-A117EF29509E","type":"LineBasicMaterial","color":16777215,"depthFunc":3,"depthTest":true,"depthWrite":true,"dithering":false}],"object":{"uuid":"D00DCA3F-F8DD-455D-A62A-FDA8F57B3482","type":"Group","matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"children":[{"uuid":"720648AE-0D07-44AF-9F32-96A1F47593A6","type":"LineSegments","name":"Circle","matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"geometry":"DA0E4DEE-D2DB-4DBA-84B9-B4EE8A8AB258","material":"83E6A459-3112-4516-AB80-A117EF29509E"}]}}; // eslint-disable-line + +densityBuoyancyCommon.register( 'DuckData', DuckData ); +export default DuckData; \ No newline at end of file Index: js/buoyancy/model/Duck.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/Duck.ts b/js/buoyancy/model/Duck.ts new file mode 100644 --- /dev/null (date 1713526720058) +++ b/js/buoyancy/model/Duck.ts (date 1713526720058) @@ -0,0 +1,248 @@ +// Copyright 2019-2024, University of Colorado Boulder + +/** + * An adjustable Duck + * + * @author Agustín Vallejo (PhET Interactive Simulations) + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +import Property from '../../../../axon/js/Property.js'; +import StrictOmit from '../../../../phet-core/js/types/StrictOmit.js'; +import Bounds3 from '../../../../dot/js/Bounds3.js'; +import Ray3 from '../../../../dot/js/Ray3.js'; +import Utils from '../../../../dot/js/Utils.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Vector3 from '../../../../dot/js/Vector3.js'; +import { Shape } from '../../../../kite/js/imports.js'; +import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; +import IOType from '../../../../tandem/js/types/IOType.js'; +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; +import Mass, { InstrumentedMassOptions, MASS_MAX_SHAPES_DIMENSION, MASS_MIN_SHAPES_DIMENSION } from '../../common/model/Mass.js'; +import PhysicsEngine from '../../common/model/PhysicsEngine.js'; +import { MassShape } from '../../common/model/MassShape.js'; +import DuckData from '../../common/data/DuckData.js'; + +export type DuckOptions = StrictOmit; + +const loader = new THREE.ObjectLoader(); + +export const mainDuckGeometry = loader.parse( DuckData ); + +const VERTICES = _.chunk( mainDuckGeometry.children[0].geometry.getAttribute('position').array, 3).map( vert3 => { + return new Vector2( vert3[0], vert3[2] ); +} ); + +export default class Duck extends Mass { + + public readonly sizeProperty: Property; + + // Step information + public stepMaximumArea: number; + public stepMaximumVolume: number; + + public constructor( engine: PhysicsEngine, size: Bounds3, providedConfig: DuckOptions ) { + + const config = optionize()( { + body: engine.createFromVertices( Duck.getDuckVertices( size.width, size.height ), false ), + shape: Duck.getDuckShape( size.width, size.height ), + volume: Duck.getVolume( size ), + massShape: MassShape.DUCK, + + phetioType: Duck.DuckIO + }, providedConfig ); + + assert && assert( !config.canRotate ); + + super( engine, config as InstrumentedMassOptions ); + + this.sizeProperty = new Property( size, { + valueType: Bounds3, + tandem: config.tandem.createTandem( 'sizeProperty' ), + phetioValueType: Bounds3.Bounds3IO + } ); + + this.stepMaximumArea = 0; + this.stepMaximumVolume = 0; + + this.updateSize( size ); + } + + public override getLocalBounds(): Bounds3 { + return this.sizeProperty.value; + } + + /** + * Updates the size of the duck. + */ + public updateSize( size: Bounds3 ): void { + this.engine.updateFromVertices( this.body, Duck.getDuckVertices( size.width, size.height ), false ); + this.sizeProperty.value = size; + this.shapeProperty.value = Duck.getDuckShape( size.width, size.height ); + + this.volumeLock = true; + this.volumeProperty.value = Duck.getVolume( size ); + this.volumeLock = false; + + this.forceOffsetProperty.value = new Vector3( 0, 0, size.maxZ ); + this.massLabelOffsetProperty.value = new Vector3( 0, size.minY * 0.5, size.maxZ * 0.7 ); + } + + /** + * Returns the general size of the mass based on a general size scale. + */ + public static getSizeFromRatios( widthRatio: number, heightRatio: number ): Bounds3 { + const x = ( MASS_MIN_SHAPES_DIMENSION + widthRatio * ( MASS_MAX_SHAPES_DIMENSION - MASS_MIN_SHAPES_DIMENSION ) ) / 2; + const y = ( MASS_MIN_SHAPES_DIMENSION + heightRatio * ( MASS_MAX_SHAPES_DIMENSION - MASS_MIN_SHAPES_DIMENSION ) ) / 2; + return new Bounds3( -x, -y, -x, x, y, x ); + } + + /** + * Sets the general size of the mass based on a general size scale. + */ + public setRatios( widthRatio: number, heightRatio: number ): void { + this.updateSize( Duck.getSizeFromRatios( widthRatio, heightRatio ) ); + } + + /** + * Called after a engine-physics-model step once before doing other operations (like computing buoyant forces, + * displacement, etc.) so that it can set high-performance flags used for this purpose. + * + * Type-specific values are likely to be set, but this should set at least stepX/stepBottom/stepTop + */ + public override updateStepInformation(): void { + super.updateStepInformation(); + + const xOffset = this.stepMatrix.m02(); + const yOffset = this.stepMatrix.m12(); + + this.stepX = xOffset; + this.stepBottom = yOffset + this.sizeProperty.value.minY; + this.stepTop = yOffset + this.sizeProperty.value.maxY; + + const a = this.sizeProperty.value.width / 2; + const b = this.sizeProperty.value.height / 2; + const c = this.sizeProperty.value.depth / 2; + this.stepMaximumArea = 4 * Math.PI * a * c; // 4 * pi * a * c + this.stepMaximumVolume = this.stepMaximumArea * b / 3; // 4/3 * pi * a * b * c + } + + /** + * If there is an intersection with the ray and this mass, the t-value (distance the ray would need to travel to + * reach the intersection, e.g. ray.position + ray.distance * t === intersectionPoint) will be returned. Otherwise + * if there is no intersection, null will be returned. + */ + public override intersect( ray: Ray3, isTouch: boolean ): number | null { + const translation = this.matrix.getTranslation().toVector3(); + const size = this.sizeProperty.value; + const relativePosition = ray.position.minusXYZ( translation.x, translation.y, translation.z ); + + const xp = 4 / ( size.width * size.width ); + const yp = 4 / ( size.height * size.height ); + const zp = 4 / ( size.depth * size.depth ); + + const a = xp * ray.direction.x * ray.direction.x + yp * ray.direction.y * ray.direction.y + zp * ray.direction.z * ray.direction.z; + const b = 2 * ( xp * relativePosition.x * ray.direction.x + yp * relativePosition.y * ray.direction.y + zp * relativePosition.z * ray.direction.z ); + const c = -1 + xp * relativePosition.x * relativePosition.x + yp * relativePosition.y * relativePosition.y + zp * relativePosition.z * relativePosition.z; + + const tValues = Utils.solveQuadraticRootsReal( a, b, c )!.filter( t => t > 0 ); + + if ( tValues.length ) { + return tValues[ 0 ]; + } + else { + return null; + } + } + + /** + * Returns the cumulative displaced volume of this object up to a given y level. + * + * Assumes step information was updated. + */ + public getDisplacedArea( liquidLevel: number ): number { + if ( liquidLevel < this.stepBottom || liquidLevel > this.stepTop ) { + return 0; + } + else { + const ratio = ( liquidLevel - this.stepBottom ) / ( this.stepTop - this.stepBottom ); + + return 0.1; // 4 * pi * a * c * ( t - t^2 ) + } + } + + /** + * Returns the displaced volume of this object up to a given y level, assuming a y value for the given liquid level. + * + * Assumes step information was updated. + */ + public getDisplacedVolume( liquidLevel: number ): number { + if ( liquidLevel <= this.stepBottom ) { + return 0; + } + else if ( liquidLevel >= this.stepTop ) { + return this.stepMaximumVolume; + } + else { + const ratio = ( liquidLevel - this.stepBottom ) / ( this.stepTop - this.stepBottom ); + + return this.stepMaximumVolume * ratio * ratio * ( 3 - 2 * ratio ); // 4/3 * pi * a * b * c * t^2 * ( 3 - 2t ) + } + } + + /** + * Resets things to their original values. + */ + public override reset(): void { + this.sizeProperty.reset(); + this.updateSize( this.sizeProperty.value ); + + super.reset(); + } + + /** + * Releases references + */ + public override dispose(): void { + this.sizeProperty.dispose(); + + super.dispose(); + } + + /** + * Returns a duck shape + */ + public static getDuckShape( width: number, height: number ): Shape { + return Shape.circle( Vector2.ZERO, height / 2 ); + } + + /** + * Returns vertices for a duck + */ + public static getDuckVertices( width: number, height: number ): Vector2[] { + + // a square + const vertices = [ + new Vector2( -width / 2, -height / 2 ), + new Vector2( width / 2, -height / 2 ), + new Vector2( width / 2, height / 2 ), + new Vector2( -width / 2, height / 2 ) + ]; + return vertices; + } + + /** + * Returns the volume of a duck with the given axis-aligned bounding box. + */ + public static getVolume( size: Bounds3 ): number { + return Math.PI * size.width * size.height * size.depth / 6; + } + + public static DuckIO = new IOType( 'DuckIO', { + valueType: Duck, + supertype: Mass.MassIO, + documentation: 'Represents a duck' + } ); +} + +densityBuoyancyCommon.register( 'Duck', Duck ); \ No newline at end of file Index: js/buoyancy/view/DuckView.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/view/DuckView.ts b/js/buoyancy/view/DuckView.ts new file mode 100644 --- /dev/null (date 1713526690789) +++ b/js/buoyancy/view/DuckView.ts (date 1713526690789) @@ -0,0 +1,46 @@ +// Copyright 2019-2024, University of Colorado Boulder + +/** + * The 3D view for a Duck. + * + * @author Agustín Vallejo + * @author Michael Kauzman + */ + +import Bounds3 from '../../../../dot/js/Bounds3.js'; +import densityBuoyancyCommon from '../../densityBuoyancyCommon.js'; +import MassView from '../../common/view/MassView.js'; +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; +import Duck, { mainDuckGeometry } from '../model/Duck.js'; +import { THREEModelViewTransform } from '../../common/view/DensityBuoyancyScreenView.js'; + +export default class DuckView extends MassView { + + public readonly duck: Duck; + private readonly duckGeometry: THREE.SphereGeometry; + + // private readonly updateListener: ( newSize: Bounds3, oldSize: Bounds3 ) => void; + + public constructor( duck: Duck, modelViewTransform: THREEModelViewTransform, dragBoundsProperty: TReadOnlyProperty ) { + + const duckGeometry = mainDuckGeometry.children[ 0 ].geometry; + + super( duck, duckGeometry, modelViewTransform, dragBoundsProperty ); + + this.duck = duck; + this.duckGeometry = duckGeometry; + const SCALE = 0.1; + this.duckGeometry.scale( SCALE, SCALE, SCALE ); + } + + /** + * Releases references. + */ + public override dispose(): void { + this.duckGeometry.dispose(); + + super.dispose(); + } +} + +densityBuoyancyCommon.register( 'DuckView', DuckView ); \ No newline at end of file Index: js/common/view/DensityBuoyancyScreenView.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/view/DensityBuoyancyScreenView.ts b/js/common/view/DensityBuoyancyScreenView.ts --- a/js/common/view/DensityBuoyancyScreenView.ts (revision c63f3caa900f636018e265ebe3ed7ad2bfdc8b8e) +++ b/js/common/view/DensityBuoyancyScreenView.ts (date 1713526510864) @@ -60,6 +60,8 @@ import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js'; import BackgroundEventTargetListener from './BackgroundEventTargetListener.js'; import MassDecorationLayer from './MassDecorationLayer.js'; +import Duck from '../../buoyancy/model/Duck.js'; +import DuckView from '../../buoyancy/view/DuckView.js'; // constants const MARGIN = DensityBuoyancyCommonConstants.MARGIN; @@ -95,6 +97,7 @@ private readonly massDecorationLayer = new MassDecorationLayer(); + // TODO: https://github.com/phetsims/buoyancy/issues/104 ok for this to be public? public readonly massViews: MassView[]; private readonly debugView?: DebugView; @@ -157,13 +160,14 @@ this.massViews = []; this.sceneNode.stage.threeCamera.zoom = options.cameraZoom; + this.sceneNode.stage.threeCamera.updateProjectionMatrix(); this.sceneNode.stage.threeCamera.up = new THREE.Vector3( 0, 0, -1 ); this.sceneNode.stage.threeCamera.lookAt( ThreeUtils.vectorToThree( options.cameraLookAt ) ); - this.sceneNode.stage.threeCamera.updateMatrixWorld( true ); - this.sceneNode.stage.threeCamera.updateProjectionMatrix(); let mouse: Mouse | null = null; + // TODO: BackgroundEventTargetListeners wants to be able to set the mouse. Is there a better way to do it? See https://github.com/phetsims/buoyancy/issues/104 + // TODO: Should this be an instance method on the prototype? And make mouse an instance variable? See https://github.com/phetsims/buoyancy/issues/104 const updateCursor = ( newMouse?: Mouse ) => { mouse = newMouse || mouse; if ( mouse ) { @@ -511,6 +515,9 @@ model.showGravityForceProperty, model.showBuoyancyForceProperty, model.showContactForceProperty, model.showForceValuesProperty, model.forceScaleProperty, model.showMassesProperty ); } + else if ( mass instanceof Duck ) { + massView = new DuckView( mass, this, dragBoundsProperty ); + } assert && assert( !!massView, `massView is falsy, mass: ${mass.constructor.name}` ); this.sceneNode.stage.threeScene.add( massView.massMesh ); @@ -784,6 +791,15 @@ image.top = 0; return image; } + + /** + * Returns an icon meant to be used as a fallback in case webgl is not available. + */ + protected static getFallbackIcon(): Node { + return new Rectangle( 0, 0, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.width, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.height, { + fill: 'gray' + } ); + } } densityBuoyancyCommon.register( 'DensityBuoyancyScreenView', DensityBuoyancyScreenView ); \ No newline at end of file Index: js/common/model/MassShape.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/model/MassShape.ts b/js/common/model/MassShape.ts --- a/js/common/model/MassShape.ts (revision c63f3caa900f636018e265ebe3ed7ad2bfdc8b8e) +++ b/js/common/model/MassShape.ts (date 1713526510875) @@ -37,6 +37,10 @@ DensityBuoyancyCommonStrings.shape.invertedConeStringProperty, 'invertedCone' ); + public static readonly DUCK = new MassShape( + DensityBuoyancyCommonStrings.shape.invertedConeStringProperty, + 'duck' + ); public constructor( public readonly shapeString: LocalizedStringProperty, public readonly tandemName: string ) {super();} Index: js/buoyancy/model/BuoyancyShapesModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/BuoyancyShapesModel.ts b/js/buoyancy/model/BuoyancyShapesModel.ts --- a/js/buoyancy/model/BuoyancyShapesModel.ts (revision c63f3caa900f636018e265ebe3ed7ad2bfdc8b8e) +++ b/js/buoyancy/model/BuoyancyShapesModel.ts (date 1713526510889) @@ -30,6 +30,7 @@ import TProperty from '../../../../axon/js/TProperty.js'; import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; import MassTag from '../../common/model/MassTag.js'; +import Duck from './Duck.js'; export type BuoyancyShapesModelOptions = DensityBuoyancyModelOptions; @@ -82,7 +83,7 @@ } ); this.availableMasses.push( this.scale1 ); - this.primaryShapeProperty = new EnumerationProperty( MassShape.BLOCK, { + this.primaryShapeProperty = new EnumerationProperty( MassShape.DUCK, { tandem: tandem.createTandem( 'primaryShapeProperty' ) } ); this.secondaryShapeProperty = new EnumerationProperty( MassShape.INVERTED_CONE, { @@ -143,6 +144,9 @@ false, massOptions ); + break; + case MassShape.DUCK: + mass = new Duck( this.engine, Duck.getSizeFromRatios( widthRatio, heightRatio ), massOptions ); break; default: throw new Error( `shape not recognized: ${shape}` );
zepumph commented 2 weeks ago

Update patch + a few files were committed

``` Subject: [PATCH] Add initial duck model/view types (not using them), https://github.com/phetsims/buoyancy/issues/101 --- Index: js/buoyancy/model/BuoyancyShapesModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/BuoyancyShapesModel.ts b/js/buoyancy/model/BuoyancyShapesModel.ts --- a/js/buoyancy/model/BuoyancyShapesModel.ts (revision ab13c2a1cedcea0ee21c1005bfa8c53a84ded125) +++ b/js/buoyancy/model/BuoyancyShapesModel.ts (date 1713553875307) @@ -30,6 +30,7 @@ import TProperty from '../../../../axon/js/TProperty.js'; import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; import MassTag from '../../common/model/MassTag.js'; +import Duck from './Duck.js'; export type BuoyancyShapesModelOptions = DensityBuoyancyModelOptions; @@ -82,7 +83,7 @@ } ); this.availableMasses.push( this.scale1 ); - this.primaryShapeProperty = new EnumerationProperty( MassShape.BLOCK, { + this.primaryShapeProperty = new EnumerationProperty( MassShape.DUCK, { tandem: tandem.createTandem( 'primaryShapeProperty' ) } ); this.secondaryShapeProperty = new EnumerationProperty( MassShape.INVERTED_CONE, { @@ -143,6 +144,9 @@ false, massOptions ); + break; + case MassShape.DUCK: + mass = new Duck( this.engine, Duck.getSizeFromRatios( widthRatio, heightRatio ), massOptions ); break; default: throw new Error( `shape not recognized: ${shape}` ); Index: js/common/view/DensityBuoyancyScreenView.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/view/DensityBuoyancyScreenView.ts b/js/common/view/DensityBuoyancyScreenView.ts --- a/js/common/view/DensityBuoyancyScreenView.ts (revision ab13c2a1cedcea0ee21c1005bfa8c53a84ded125) +++ b/js/common/view/DensityBuoyancyScreenView.ts (date 1713555624882) @@ -60,6 +60,8 @@ import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js'; import BackgroundEventTargetListener from './BackgroundEventTargetListener.js'; import MassDecorationLayer from './MassDecorationLayer.js'; +import Duck from '../../buoyancy/model/Duck.js'; +import DuckView from '../../buoyancy/view/DuckView.js'; // constants const MARGIN = DensityBuoyancyCommonConstants.MARGIN; @@ -510,6 +512,11 @@ massView = new BoatView( mass, this, dragBoundsProperty, model.pool.liquidYInterpolatedProperty, model.showGravityForceProperty, model.showBuoyancyForceProperty, model.showContactForceProperty, model.showForceValuesProperty, model.forceScaleProperty, model.showMassesProperty ); + } + else if ( mass instanceof Duck ) { + massView = new DuckView( mass, this, dragBoundsProperty, + model.showGravityForceProperty, model.showBuoyancyForceProperty, model.showContactForceProperty, + model.showForceValuesProperty, model.forceScaleProperty, model.showMassesProperty ); } assert && assert( !!massView, `massView is falsy, mass: ${mass.constructor.name}` ); @@ -623,7 +630,7 @@ const modelPoint = this.viewToModelPoint( viewPoint ); const newViewPoint = this.parentToLocalPoint( animatedPanZoomSingleton.listener.matrixProperty.value.inverted().timesVector2( this.sceneNode.projectPoint( modelPoint ) ) ); - assert && assert( newViewPoint.minus( viewPoint ).getMagnitude() < 1e-3, `model/view point transform difference: ${viewPoint}, and ${newViewPoint}` ); + // assert && assert( newViewPoint.minus( viewPoint ).getMagnitude() < 1e-3, `model/view point transform difference: ${viewPoint}, and ${newViewPoint}` ); } return viewPoint; }
pixelzoom commented 2 weeks ago

@zepumph git-hooks are failing, so I regenerated the density API file in https://github.com/phetsims/phet-io-sim-specific/commit/d55438c197914e1b30415c30ee8ce46454bad151. How did this get past git-hooks?

density BREAKING PROBLEMS
Type missing: EnumerationIO(BLOCK|ELLIPSOID|VERTICAL_CYLINDER|HORIZONTAL_CYLINDER|CONE|INVERTED_CONE)
density DESIGN PROBLEMS
New PhET-iO Element not in reference: density.general.model.strings.densityBuoyancyCommon.shape.duckStringProperty
zepumph commented 1 week ago

Right. Thanks @pixelzoom for the commit.

We are also going to want to figure out how best to support migration problems with Density. That sim doesn't have a duck, but MassShape does, and adding that entry to the EnumerationIO has made a bunch of changes to the API (see above). I don't know that we want to have that in Density, but also it isn't clear what is the easier path (let alone desirable).

AgustinVallejo commented 1 week ago

Been investigating into creating the Duck's flat geometry and it's been way more complicated than I anticipated. The following are a couple of patches that get this (sort of?) working. Sort of because they either clip through the floor, or depend on hard computed numbers, or raise assertions, or are a convex hull and not the concave duck shape.

I think the most successful one is the second, but it didn't get the accurate duck geometry.

```diff Subject: [PATCH] Real flat geometry of duck --- Index: js/buoyancy/model/Duck.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/Duck.ts b/js/buoyancy/model/Duck.ts --- a/js/buoyancy/model/Duck.ts (revision ba5af3d4b4bd59ee56da40f787a7db1eff4c79c9) +++ b/js/buoyancy/model/Duck.ts (date 1713931435117) @@ -32,6 +32,87 @@ const duckGeometry = ( mainDuckGeometry.children[ 0 ] as THREE.Mesh ).geometry.scale( SCALE, SCALE, SCALE ); +let vertices = [ + new Vector2( 0.024127, -0.985022 ), + new Vector2( -0.065763, -1.010711 ), + new Vector2( -0.29008, -1.012616 ), + new Vector2( -0.354542, -1.001538 ), + new Vector2( -0.385327, -0.963505 ), + new Vector2( -0.371228, -0.897462 ), + new Vector2( -0.226988, -0.834853 ), + new Vector2( -0.23751, -0.716437 ), + new Vector2( -0.343253, -0.706173 ), + new Vector2( -0.447034, -0.656992 ), + new Vector2( -0.541834, -0.553407 ), + new Vector2( -0.609499, -0.440947 ), + new Vector2( -0.637335, -0.325621 ), + new Vector2( -0.628668, -0.215376 ), + new Vector2( -0.591497, -0.114765 ), + new Vector2( -0.537008, -0.013863 ), + new Vector2( -0.430042, 0.214967 ), + new Vector2( -0.46972, 0.274876 ), + new Vector2( -0.518352, 0.330276 ), + new Vector2( -0.635876, 0.427949 ), + new Vector2( -0.657909, 0.456598 ), + new Vector2( -0.660824, 0.485854 ), + new Vector2( -0.612145, 0.514872 ), + new Vector2( -0.512324, 0.511485 ), + new Vector2( -0.457111, 0.533535 ), + new Vector2( -0.405968, 0.601713 ), + new Vector2( -0.357356, 0.67749 ), + new Vector2( -0.306776, 0.73531 ), + new Vector2( -0.247754, 0.773244 ), + new Vector2( -0.17106, 0.795093 ), + new Vector2( -0.093403, 0.801028 ), + new Vector2( -0.035849, 0.804496 ), + new Vector2( 0.027891, 0.792761 ), + new Vector2( 0.092705, 0.770376 ), + new Vector2( 0.163772, 0.742717 ), + new Vector2( 0.215767, 0.711891 ), + new Vector2( 0.261052, 0.672716 ), + new Vector2( 0.305289, 0.616332 ), + new Vector2( 0.33883, 0.531846 ), + new Vector2( 0.336206, 0.5276 ), + new Vector2( 0.347608, 0.417499 ), + new Vector2( 0.337073, 0.306564 ), + new Vector2( 0.298522, 0.240446 ), + new Vector2( 0.269807, 0.170258 ), + new Vector2( 0.260725, 0.10015 ), + new Vector2( 0.284936, 0.038505 ), + new Vector2( 0.34165, -0.005539 ), + new Vector2( 0.419488, -0.004722 ), + new Vector2( 0.494726, 0.02512 ), + new Vector2( 0.56651, 0.07802 ), + new Vector2( 0.727214, 0.223658 ), + new Vector2( 0.755921, 0.231461 ), + new Vector2( 0.783558, 0.208392 ), + new Vector2( 0.8071, 0.164325 ), + new Vector2( 0.859687, -0.136826 ), + new Vector2( 0.870634, -0.346609 ), + new Vector2( 0.849003, -0.486313 ), + new Vector2( 0.798585, -0.567414 ), + new Vector2( 0.726078, -0.627812 ), + new Vector2( 0.638086, -0.670727 ), + new Vector2( 0.597362, -0.682948 ), + new Vector2( 0.597362, -0.985022 ), + new Vector2( 0.507472, -1.010711 ), + new Vector2( 0.283155, -1.012616 ), + new Vector2( 0.218693, -1.001538 ), + new Vector2( 0.187908, -0.963505 ), + new Vector2( 0.202007, -0.897462 ), + new Vector2( 0.346248, -0.834853 ), + new Vector2( 0.335938, -0.719925 ), + new Vector2( 0.024127, -0.720342 ) +]; + +vertices = vertices.map( vertex => { + return vertex.multiplyScalar( SCALE ); +} ); + +const getProjectedPoints = () => { + return vertices; +}; + // const VERTICES = _.chunk( mainDuckGeometry.children[ 0 ].geometry.getAttribute( 'position' ).array, 3 ).map( vert3 => { // return new Vector2( vert3[ 0 ], vert3[ 2 ] ); @@ -217,23 +298,19 @@ * Returns a duck shape */ public static getDuckShape( width: number, height: number ): Shape { - // Maybe get a 2d shape via code in the patch in https://github.com/phetsims/density-buoyancy-common/issues/115#issuecomment-2067166189 - return Shape.circle( Vector2.ZERO, height / 2 ); + + const projectedVertices = getProjectedPoints(); + + // ConvexHull2.grahamScan( projectedVertices, false ) + + return Shape.polygon( projectedVertices ); } /** * Returns vertices for a duck */ public static getDuckVertices( width: number, height: number ): Vector2[] { - - // a square - const vertices = [ - new Vector2( -width / 2, -height / 2 ), - new Vector2( width / 2, -height / 2 ), - new Vector2( width / 2, height / 2 ), - new Vector2( -width / 2, height / 2 ) - ]; - return vertices; + return getProjectedPoints(); } /** ```
```diff Subject: [PATCH] Convex hull and duck geometry --- Index: js/buoyancy/model/Duck.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/buoyancy/model/Duck.ts b/js/buoyancy/model/Duck.ts --- a/js/buoyancy/model/Duck.ts (revision ba5af3d4b4bd59ee56da40f787a7db1eff4c79c9) +++ b/js/buoyancy/model/Duck.ts (date 1713931737822) @@ -22,6 +22,7 @@ import PhysicsEngine from '../../common/model/PhysicsEngine.js'; import { MassShape } from '../../common/model/MassShape.js'; import DuckData from './DuckData.js'; +import ConvexHull2 from '../../../../dot/js/ConvexHull2.js'; export type DuckOptions = StrictOmit; @@ -217,8 +218,21 @@ * Returns a duck shape */ public static getDuckShape( width: number, height: number ): Shape { - // Maybe get a 2d shape via code in the patch in https://github.com/phetsims/density-buoyancy-common/issues/115#issuecomment-2067166189 - return Shape.circle( Vector2.ZERO, height / 2 ); + + const vertices: Vector2[] = []; + + const geometry = duckGeometry; + + if ( geometry.isBufferGeometry && geometry.attributes.position ) { + // Handle BufferGeometry + const positions = geometry.attributes.position; + for ( let i = 0; i < positions.count; i++ ) { + const vertex = new Vector2( positions.getX( i ), positions.getY( i ) ); + vertices.push( vertex ); + } + } + + return Shape.polygon( ConvexHull2.grahamScan( vertices, false ) ); } /**
AgustinVallejo commented 1 week ago

For reference, here are the two patches side by side... The first one is misleading, because to get that to work, it was like an hour of wrestling the geometry data and literally writing a recursive python script to sort the points (and then sort some manually) to get it to behave like a good polygon. The second one is just auto generated from the duck geometry.

image

zepumph commented 1 week ago
AgustinVallejo commented 1 week ago

After talking with @zepumph it seems the first duck shape is preferrable, in spite of the difficulty to replicate, which I'll describe here briefly before moving all these steps into its own documentation file.

Goal: Get from the 3D duck mesh into a 2D projection of it we can use in the p2 model. Difficulty: It's not as easy as it seems. 3D to 2D projection algorithms apparently work really well with convex shapes, but not with a shape like the duck. Disclaimer: There's likely and hopefully easier ways to achieve this, and it's probably worth asking in the internet or to other devs (@jonathanolson maybe). But for now this is the best way I could get it working. Discussion and advice is welcome. Steps:

  1. Squish the 3D shape into 2D. This can be done in Blender or just iterating over the vertices and saving their XY or XZ coordinates (XY in THREE.js, XZ in Blender).
  2. Manually (?) removing all the vertices that ended up inside the duck shape. Supposedly Blender can do this automatically, but I couldn't get it to work. Programatically it's way harder since with only the array of points, we have no way of knowing which ones are "inside". That's why Convex Hull algorithms botch this step.
  3. If you're lucky, the resulting vertices are ordered properly for a Polygon. Otherwise, below is a python script which reorders them in a proper line, by closeness. Again, there's likely algorithms that already do this better but I was just brute-forcing my way through it. 3.1. Even with the algorithm, it got one foot backwards, so I had to manually re order like 5 vertices.
  4. Once the vertices are ordered, create a polygon and enjoy your 2d duck.
```python # Original array of vertices vertices = [] # Put here the tuples of vertex coordinates, i.e. ( 0.53, 1.22 ) # New vertices empty array vertices2 = [] c = vertices[0] # First vertex # Distance function dist = lambda a,b: ((a[0] - b[0])**2 + (a[1] - b[1])**2)**.5 count = 0 while ( len(vertices2) < len(vertices) ) or ( count > len( vertices ) * 5): if c not in vertices2: print(f"Percentage of vertices added {100*len(vertices2)/len(vertices)}%") # NOTE: If this percentage stalls, feel free to interrupt the code. # There were likely repeated vertices and you can proceed with vertices2 vertices2.append(c) minim = 100 # Minimum distance, will change ctemp = () # New coordinates to be added for v in vertices: # Check all the vertices for distance if v != c: d = dist(v,c) if d < minim and v not in vertices2: minim = d ctemp = v if ctemp: c = ctemp count += 1
AgustinVallejo commented 5 days ago

@DianaTavares please review.

Will be looking into a remaining duck dancing bug in https://github.com/phetsims/buoyancy/issues/148

DianaTavares commented 4 days ago

Sorry, I forgot to answer this issue!! Well, the duck is super cute!! But @AgustinVallejo needs to review the volume because now it is very small. Comparing the duck with the bigger Block, looks like the duck needs to be less that 10L, probably like 5 to 7?

AgustinVallejo commented 4 days ago

@DianaTavares and I decided to hollywood the Duck's displayed volume to be 5L at maximum.