UNPKG

agentscript

Version:

AgentScript Model in Model/View architecture

358 lines (322 loc) 12.8 kB
import * as util from './utils.js' import World from './World.js' import SpriteSheet from './SpriteSheet.js' import PatchesView from './PatchesView.js' import ThreeMeshes from './ThreeMeshes.js' import { THREE, OrbitControls } from '../vendor/three.js' class ThreeView { static shapeNames() { return ThreeMeshes.Obj3DShapes } static defaultOptions(useThreeHelpers = true) { const options = { div: document.body, orthoView: false, // 'Perspective', 'Orthographic' clearColor: 0x000000, // clear to black // clearColor: new THREE.Color(0x000000), // clear to black useAxes: useThreeHelpers, // show x,y,z axes useGrid: useThreeHelpers, // show x,y plane // useControls: useThreeHelpers, // navigation. REMIND: control name? useWorldOutline: useThreeHelpers, // useStats: useThreeHelpers, // stats fps ui useLights: true, // maybe should be mesh option? not view? // REMIND: put in quadsprite options, defaulting to 64 spriteSize: 64, patches: { meshClass: 'PatchesMesh', }, turtles: { meshClass: 'QuadSpritesMesh', }, links: { meshClass: 'LinksMesh', }, } return options } // ----------------------------------------------- // div? or can? // https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer // world can be options or a world instance, both work. constructor( // div = document.body, world = World.defaultOptions(), options = {} // overrides of defaultOptions ) { // options: override defaults: options = Object.assign(ThreeView.defaultOptions(), options) options.useLights = options.useLights || options.turtles.meshClass === 'Obj3DMesh' // this.div = options.div // if (util.isString(this.div)) // this.div = document.getElementById(this.div) // this.div = util.isString(div) ? document.getElementById(div) : div // this.world = new World(world) this.div = util.isString(options.div) ? document.getElementById(options.div) : options.div // If div height not set, default to 600px if (!this.div.height) this.div.style.height = '600px' this.world = new World(world.world || world) // world can be model this.options = options this.ticks = 0 if (this.options.spriteSize !== 0) { const isPOT = util.isPowerOf2(this.options.spriteSize) this.spriteSheet = new SpriteSheet( this.options.spriteSize, 16, isPOT ) } if (options.patches && options.patches.meshClass === 'PatchesMesh') { this.patchesView = new PatchesView( this.world.width, this.world.height ) } this.initThree() this.initThreeHelpers() this.initMeshes() } // Init Three.js core: scene, camera, renderer initThree() { const { clientWidth, clientHeight } = this.div const { orthoView, clearColor } = this.options // const {width, height, centerX, centerY} = this.world // const { width, height } = this.world const [width, height] = this.world.getWorldSize() const [halfW, halfH] = [width / 2, height / 2] // this.spriteSheet.texture = new THREE.CanvasTexture(this.spriteSheet.ctx) // this.spriteSheet.setTexture(THREE.CanvasTexture) // REMIND: need world.minZ/maxZ const orthographicCam = new THREE.OrthographicCamera( // const orthographicCam = new OrthographicCamera( -halfW, halfW, halfH, -halfH, 1, 20 * width ) orthographicCam.position.set(0, 0, 10 * width) orthographicCam.up.set(0, 0, 1) const perspectiveCam = new THREE.PerspectiveCamera( 45, clientWidth / clientHeight, 0.1, 10000 ) // perspectiveCam.position.set(width + centerX, -width - centerY, width) // perspectiveCam.position.set(width, -width, width) perspectiveCam.position.set(width, -width, 1.2 * width) // perspectiveCam.position.set(width, width, width) // perspectiveCam.lookAt(new THREE.Vector3(centerX, centerY, 0)) perspectiveCam.up.set(0, 0, 1) const scene = new THREE.Scene() // scene.background = clearColor // scene.position = new THREE.Vector3(centerX, centerY, 0) const camera = orthoView ? orthographicCam : perspectiveCam // if (orthoView) // camera.position.set(0, 0, 100 * width) // else // camera.position.set(width, -width, width) // camera.up.set(0, 0, 1) // const renderer = new THREE.WebGLRenderer({ canvas: this.div }) // const isCanvas = util.isCanvas(this.div) // const threeOpts = isCanvas ? { canvas: this.div } : {} // const renderer = new THREE.WebGLRenderer(threeOpts) const renderer = new THREE.WebGLRenderer() renderer.setPixelRatio(window.devicePixelRatio) renderer.setSize(clientWidth, clientHeight) renderer.setClearColor(clearColor) // if (!isCanvas) this.div.appendChild(renderer.domElement) this.div.appendChild(renderer.domElement) this.orbitControls = new OrbitControls(camera, renderer.domElement) // window.addEventListener('resize', () => { // const {clientWidth, clientHeight} = this.model.div // camera.aspect = clientWidth / clientHeight // camera.updateProjectionMatrix() // renderer.setSize(clientWidth, clientHeight) // }) window.addEventListener('resize', () => { this.resize() }) Object.assign(this, { scene, camera, renderer, orthographicCam, perspectiveCam, }) } resize() { const { clientWidth, clientHeight } = this.div const [width, height] = this.world.getWorldSize() // w/o "patchSize" if (this.options.orthoView) { const zoom = Math.min(clientWidth / width, clientHeight / height) this.renderer.setSize(zoom * width, zoom * height) } else { this.camera.aspect = clientWidth / clientHeight this.camera.updateProjectionMatrix() this.renderer.setSize(clientWidth, clientHeight) } } toggleCamera() { this.options.orthoView = !this.options.orthoView if (this.options.orthoView) { this.camera = this.orthographicCam } else { this.camera = this.perspectiveCam } this.resize() this.renderer.render(this.scene, this.camera) } // Return a dataURL for the current model step. snapshot(useOrtho = true) { // Don't set camera, can change w/ toggle below const { scene, renderer, model } = this const toggle = useOrtho && this.camera === this.perspectiveCam if (toggle) { this.toggleCamera() // model.draw(true) REMIND, need a draw proc } renderer.render(scene, this.camera) const durl = renderer.domElement.toDataURL() if (toggle) this.toggleCamera() return durl } initThreeHelpers() { const { scene, renderer, camera, world } = this // const {useAxes, useGrid, useControls, useStats, useGUI} = this const { useAxes, useGrid, // useControls, // useStats, useLights, useWorldOutline, } = this.options const { width, height, depth, minZ } = this.world const helpers = {} if (useAxes) { helpers.axes = new THREE.AxesHelper((1.5 * width) / 2) scene.add(helpers.axes) } if (useGrid) { helpers.grid = new THREE.GridHelper(1.25 * width, 10) helpers.grid.rotation.x = THREE.Math.degToRad(90) helpers.grid.position.z = minZ scene.add(helpers.grid) } // if (useControls) { // // helpers.controls = new THREE.OrbitControls( // helpers.controls = new OrbitControls(camera, renderer.domElement) // } // if (useStats) { // helpers.stats = new Stats() // document.body.appendChild(helpers.stats.dom) // } if (useLights) { const width = world.width helpers.directionalLight = new THREE.DirectionalLight(0xffffff, 1) helpers.directionalLight.position.set(width, width, width) scene.add(helpers.directionalLight) helpers.diffuseLight = new THREE.AmbientLight(0x404040) // soft white light scene.add(helpers.diffuseLight) } if (useWorldOutline) { const geometry = new THREE.BoxBufferGeometry(width, height, depth) const edges = new THREE.EdgesGeometry(geometry) helpers.outline = new THREE.LineSegments( edges, new THREE.LineBasicMaterial({ color: 0x80808080 }) ) scene.add(helpers.outline) } this.helpers = helpers // Object.assign(this, helpers) } initMeshes() { this.meshes = {} util.forLoop(this.options, (val, key) => { if (val.meshClass && val.meshClass !== 'NullMesh') { // if (val.meshClass === 'NullMesh') const Mesh = ThreeMeshes[val.meshClass] const options = val // val.options // null ok // const options = Mesh.options() // default options // // override by user's // if (val.options) Object.assign(options, val.options) // const mesh = new ThreeMeshes[val.meshClass](this, options) const mesh = new Mesh(this, options) this.meshes[key] = mesh mesh.init() // can be called again by modeler } }) } // Call this right after ctor. Or add options to default params setPatchesSmoothing(smooth = false) { const filter = smooth ? THREE.LinearFilter : THREE.NearestFilter this.meshes.patches.mesh.material.map.magFilter = filter } idle(ms = 32) { util.timeoutLoop(() => this.render(), -1, ms) } render() { // REMIND: generalize. this.renderer.render(this.scene, this.camera) this.ticks++ // if (this.helpers.stats) this.helpers.stats.update() // if (this.view.stats) this.view.stats.update() } getSprite(shape, fillColor, strokeColor = null) { return this.spriteSheet.getSprite(shape, fillColor, strokeColor) } // Sugar if viewFcn is a constant obj, convert to fcn. checkViewFcn(viewFcn) { return util.isObject(viewFcn) ? () => viewFcn : viewFcn } patchesCanvas() { return this.patchesView.ctx.canvas } clearPatches(color) { // color can be typed, pixel, css, or undefined (clear to transparent) this.patchesView.clear(color) // no args: just merge pixels into canvas, set mesh needsUpdate true this.meshes.patches.update() } // drawPatchesImage(img) { drawPatchesImage(img) { // this.meshes.patches.options.textureOptions = { const options = this.meshes.patches.options options.textureOptions = { minFilter: THREE.NearestFilter, magFilter: THREE.LinearFilter, } options.canvas = img this.meshes.patches.init() } createPatchPixels(pixelFcn) { this.patchesView.createPixels(pixelFcn) const data = this.patchesView.pixels this.meshes.patches.update(data, d => d) } drawPatches(data, viewFcn) { // REMIND: may not be needed, patchesView does this check too. if (util.isOofA(data)) data = util.toAofO(data) this.meshes.patches.update(data, viewFcn) } drawTurtles(data, viewFcn) { if (util.isOofA(data)) data = util.toAofO(data) viewFcn = this.checkViewFcn(viewFcn) this.meshes.turtles.update(data, viewFcn) } drawLinks(data, viewFcn) { if (util.isOofA(data)) data = util.toAofO(data) viewFcn = this.checkViewFcn(viewFcn) this.meshes.links.update(data, viewFcn) } } export default ThreeView /* - patches smoothing: set textureOptions to linear for mag filter */