+
+
+
+ );
+
+ function handleChange(part: 'x' | 'y' | 'z') {
+ return (newValue: number) => {
+ if (onChange) {
+ switch (part) {
+ case 'x':
+ onChange(new Vector3(newValue, value.y, value.z));
+ break;
+ case 'y':
+ onChange(new Vector3(value.x, newValue, value.z));
+ break;
+ case 'z':
+ onChange(new Vector3(value.x, value.y, newValue));
+ break;
+ }
+ }
+ }
+ }
+}
+
+export function CheckboxInput(props: { label: string, value: boolean, onChange: (value: boolean) => void }) {
+
+ return (
+
+
+
+ );
+
+ function handleChange(e: any) {
+ props.onChange && props.onChange(e.target.checked);
+ }
+}
\ No newline at end of file
diff --git a/generator/components/GithubCorner.tsx b/generator/components/GithubCorner.tsx
new file mode 100644
index 00000000..20e26b12
--- /dev/null
+++ b/generator/components/GithubCorner.tsx
@@ -0,0 +1,52 @@
+
+export default function GitHubCorner() {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/generator/components/Layout.tsx b/generator/components/Layout.tsx
new file mode 100644
index 00000000..5b4f5f4e
--- /dev/null
+++ b/generator/components/Layout.tsx
@@ -0,0 +1,13 @@
+import Head from "next/head";
+import Container from 'react-bootstrap/Container';
+import GithubCorner from './GithubCorner';
+
+export default function Layout(props: {children: any[]}) {
+ return <>
+ World Generation
+
+
+ {props.children}
+
+ >;
+}
\ No newline at end of file
diff --git a/generator/components/SceneDisplay.tsx b/generator/components/SceneDisplay.tsx
new file mode 100644
index 00000000..970050ac
--- /dev/null
+++ b/generator/components/SceneDisplay.tsx
@@ -0,0 +1,22 @@
+
+import { useLayoutEffect, useRef } from 'react';
+import SceneManager from '../services/base-scene-manager';
+
+export default function SceneDisplay ({sceneManager}: {sceneManager: SceneManager}) {
+ const canvasRef = useRef(null);
+
+ if (process.browser) {
+ useLayoutEffect(() => {
+ console.log('Starting scene...');
+ sceneManager.init(canvasRef.current);
+ sceneManager.start();
+
+ return () => {
+ console.log('Stopping scene...');
+ sceneManager.stop();
+ };
+ }, []);
+ }
+
+ return
+}
\ No newline at end of file
diff --git a/generator/components/SubPage.tsx b/generator/components/SubPage.tsx
new file mode 100644
index 00000000..12789798
--- /dev/null
+++ b/generator/components/SubPage.tsx
@@ -0,0 +1,15 @@
+import Link from "next/link";
+import Row from "react-bootstrap/Row";
+import Col from "react-bootstrap/Col";
+import Layout from "./Layout";
+
+export default function SubPage ({ header, children }: {header: string, children: any }) {
+ return (
+
+
+
Hello {header}
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/generator/components/panels/GraphicsPanel.tsx b/generator/components/panels/GraphicsPanel.tsx
new file mode 100644
index 00000000..56a6adb6
--- /dev/null
+++ b/generator/components/panels/GraphicsPanel.tsx
@@ -0,0 +1,12 @@
+import { CheckboxInput, NumberSlider } from "../FieldEditors";
+import { PlanetEditorState } from "../../hooks/use-planet-editor-state";
+
+export default function GraphicsPanel({ planetState }: { planetState: PlanetEditorState }) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/generator/components/panels/InfoPanel.tsx b/generator/components/panels/InfoPanel.tsx
new file mode 100644
index 00000000..fe42d44e
--- /dev/null
+++ b/generator/components/panels/InfoPanel.tsx
@@ -0,0 +1,33 @@
+import { PlanetEditorState } from "../../hooks/use-planet-editor-state";
+import { NumberSlider, TextBox, SeedInput, ColorPicker } from "../FieldEditors";
+import Form from "react-bootstrap/Form";
+import Col from 'react-bootstrap/Col';
+
+export default function InfoPanel({ planetState }: { planetState: PlanetEditorState }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+ >
+ );
+}
\ No newline at end of file
diff --git a/generator/components/panels/LayerPanel.tsx b/generator/components/panels/LayerPanel.tsx
new file mode 100644
index 00000000..ce7b4ce9
--- /dev/null
+++ b/generator/components/panels/LayerPanel.tsx
@@ -0,0 +1,137 @@
+import Col from 'react-bootstrap/Col';
+import Form from 'react-bootstrap/Form';
+import Button from 'react-bootstrap/Button';
+import DropdownButton from 'react-bootstrap/DropdownButton';
+import Dropdown from 'react-bootstrap/Dropdown';
+import ListGroup from 'react-bootstrap/ListGroup';
+import Octicon, { Trashcan } from '@primer/octicons-react';
+import { NumberSlider, Vector2Slider, Vector3Slider } from '../FieldEditors';
+import { PlanetLayer, MaskTypes, createContintentNoise, createMountainNoise, NoiseSettings } from '../../models/planet-settings';
+import { PlanetEditorState } from '../../hooks/use-planet-editor-state';
+import { guid } from '../../services/helpers';
+
+export default function LayerPanel({ planetState }: { planetState: PlanetEditorState }) {
+ return (
+
+ {planetState.layers.current.map((layer, i) => (
+
+
+
+
+ Label:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i > 0 ?
+ Mask:
+
+ : null }
+
+ ))}
+
+
+ Terrain Presets
+ Continents
+ Mountains
+ Plataues
+ Hills
+ Atmosphere Presets
+ Light Clouds
+ Dense Clouds
+ Hurricans
+ Orbital Presets
+ Small Rings
+ Large Rings
+
+
+
+ );
+
+ function addLayer(type: string) {
+ return function () {
+ planetState.layers.push({
+ id: guid(),
+ name: '',
+ enabled: true,
+ maskType: planetState.layers.current.length === 0 ? MaskTypes.None : MaskTypes.FirstLayer,
+ noiseSettings: type === 'Continents' ? createContintentNoise() : createMountainNoise()
+ });
+ }
+ }
+
+ function removeLayer(index: number) {
+ return function () {
+ planetState.layers.removeAt(index);
+ }
+ }
+
+ function handleNoiseChange(field: keyof NoiseSettings, layer: PlanetLayer, index: number) {
+ let fields = { ...layer };
+
+ return function (value: any) {
+ fields.noiseSettings[field] = value;
+ planetState.layers.update(index, fields);
+ };
+ }
+
+ function handleLayerChange(layer: PlanetLayer, index: number) {
+
+ let fields = { ...layer };
+
+ return function (e: any) {
+ //console.log(`${e.target.name} -> ${e.target.value}`);
+ switch (e.target.name) {
+ case 'enabled':
+ fields.enabled = e.target.checked;
+ break;
+ case 'maskType':
+ fields.maskType = e.target.value;
+ }
+
+ planetState.layers.update(index, fields);
+ };
+ }
+}
\ No newline at end of file
diff --git a/generator/hooks/use-planet-editor-state.ts b/generator/hooks/use-planet-editor-state.ts
new file mode 100644
index 00000000..8cecef44
--- /dev/null
+++ b/generator/hooks/use-planet-editor-state.ts
@@ -0,0 +1,146 @@
+import { useEffect } from 'react';
+
+import { Planet } from '../models/planet';
+import { randomSeed, guid } from '../services/helpers';
+import { MaskTypes, createContintentNoise, PlanetLayer } from '../models/planet-settings';
+import { useStatePersisted } from './use-state-persisted';
+import { useStateArray, StateArray } from './use-state-array';
+
+export type StateInstance = { current: T, set(value: T): void };
+
+function usePlanetEditorFieldState(key: string, initialValue: T, map?: (value: T) => T) {
+ const [current, set] = useStatePersisted(`world-gen:planet-editor:${key}`, initialValue);
+
+ function mappedSet(value: T) {
+ if (map) value = map(value);
+ set(value);
+ }
+
+ return { current, set: mappedSet };
+}
+
+export function usePlanetEditorState(planet: Planet): PlanetEditorState {
+
+ const name = usePlanetEditorFieldState('name', '', value => {
+ planet.name = value;
+ return planet.name;
+ });
+
+ const seed = usePlanetEditorFieldState('seed', randomSeed(), value => {
+ planet.seed = value;
+ planet.regenerateTerrain();
+ planet.regenerateShading();
+ return planet.seed;
+ });
+
+ const radius = usePlanetEditorFieldState('radius', 2, value => {
+ planet.surface.radius = value;
+ planet.regenerateTerrain();
+ planet.regenerateShading();
+ return planet.surface.radius;
+ });
+
+ const seaLevel = usePlanetEditorFieldState('seaLevel', 1, value => {
+ planet.sea.radius = value;
+ planet.sea.regenerateTerrain();
+ planet.sea.regenerateShading();
+ return planet.sea.radius;
+ });
+
+ const seaColor = usePlanetEditorFieldState('seaColor', '#0D1086', value => {
+ planet.sea.color = value;
+ planet.sea.regenerateShading();
+ return planet.sea.color;
+ });
+
+ const colors = usePlanetEditorFieldState('colors', '#2D6086', value => {
+ planet.surface.regenerateShading();
+ return value;
+ });
+
+ const layers = useStateArray([{
+ id: guid(),
+ name: '',
+ enabled: true,
+ maskType: MaskTypes.None,
+ noiseSettings: createContintentNoise()
+ }], value => {
+ planet.surface.terrainLayers = value;
+ //planet.regenerateMesh();
+ planet.surface.regenerateTerrain();
+ planet.surface.regenerateShading();
+ return planet.surface.terrainLayers;
+ });
+
+ const wireframes = usePlanetEditorFieldState('wireframes', true, value => {
+ planet.surface.wireframes = value;
+ return planet.surface.wireframes;
+ });
+
+ const resolution = usePlanetEditorFieldState('resolution', 64, value => {
+ planet.surface.resolution = Math.max(2, value);
+ planet.sea.resolution = planet.surface.resolution * 2;
+ planet.regenerateMesh();
+ planet.regenerateTerrain();
+ planet.regenerateShading();
+ return planet.surface.resolution;
+ });
+
+ const rotate = usePlanetEditorFieldState('rotate', 0.21, value => {
+ planet.rotate = value;
+ return planet.rotate;
+ });
+
+ useEffect(() => {
+ console.log(`Setting initial planet settings...`);
+ planet.name = name.current;
+ planet.seed = seed.current;
+ planet.surface.radius = radius.current;
+
+ planet.sea.radius = seaLevel.current;
+ planet.sea.color = seaColor.current;
+
+ planet.surface.terrainLayers = layers.current;
+
+ planet.rotate = rotate.current;
+ planet.surface.resolution = resolution.current;
+ planet.surface.wireframes = wireframes.current;
+
+ planet.regenerateMesh();
+ planet.regenerateTerrain();
+ planet.regenerateShading();
+ }, []);
+
+ return {
+ name,
+ colors,
+ radius,
+ seaLevel,
+ seaColor,
+
+ seed,
+
+ layers,
+
+ wireframes,
+ resolution,
+ rotate
+ };
+}
+
+export interface PlanetEditorState {
+ name: StateInstance;
+ seed: StateInstance;
+
+ colors: StateInstance;
+ radius: StateInstance;
+
+ seaLevel: StateInstance;
+ seaColor: StateInstance;
+
+ layers: StateArray,
+
+ wireframes: StateInstance;
+ resolution: StateInstance;
+ rotate: StateInstance;
+}
\ No newline at end of file
diff --git a/generator/hooks/use-state-array.ts b/generator/hooks/use-state-array.ts
new file mode 100644
index 00000000..8ecbd1af
--- /dev/null
+++ b/generator/hooks/use-state-array.ts
@@ -0,0 +1,77 @@
+import { useState, Dispatch, SetStateAction } from 'react';
+
+import { useStatePersisted } from './use-state-persisted';
+
+export interface StateArray {
+ current: T[];
+ set(value: T[]): void;
+ /**
+ * Adds an item to the end of the array.
+ * @param item The item to add.
+ */
+ push(item: T): void;
+ /**
+ * Adds an item to the start of the array.
+ * @param item The item to add.
+ */
+ unshift(item: T): void;
+ removeAt(index: number): void;
+ removeWhere(predicate: (value: T, index: number, array: T[]) => value is T): void;
+ clear(): void;
+ update(index: number, fields: Partial): void
+}
+
+/**
+ * Creates a state and setter function that persists the array to localStorage.
+ * @param key The key to use for localStorage.
+ * @param initialValue The initial array value to use.
+ * @param map An optional custom map function to modify.
+ */
+export function useStateArrayPersisted(key: string, initialValue: T[] = [], map?: (newState: T[]) => T[]): StateArray {
+ const [current, set] = useStatePersisted(key, initialValue);
+
+ return _useStateArrayLogic(current, set, map);
+}
+
+/**
+ * Creates a state and setter function for the array.
+ * @param initialValue The initial value to use.
+ * @param map An optional custom map function to modify.
+ */
+export function useStateArray(initialValue: T[] = [], map?: (newState: T[]) => T[]): StateArray {
+ const [current, set] = useState(initialValue);
+
+ return _useStateArrayLogic(current, set, map);
+}
+
+function _useStateArrayLogic(current: T[], setArr: Dispatch>, map?: any): StateArray {
+ return {
+ current,
+ set(value: T[]) {
+ mappedSet(value);
+ },
+ push(item: T) {
+ mappedSet([...current, item]);
+ },
+ unshift(item: T) {
+ mappedSet([item, ...current]);
+ },
+ removeAt(index: number) {
+ mappedSet([...current.slice(0, index), ...current.slice(index + 1)]);
+ },
+ removeWhere(predicate: (value: T, index: number, array: T[]) => value is T) {
+ mappedSet(current.filter(predicate));
+ },
+ clear() {
+ mappedSet([])
+ },
+ update(index: number, fields: Partial) {
+ mappedSet([...current.slice(0, index), { ...current[index], ...fields }, ...current.slice(index + 1)]);
+ }
+ };
+
+ function mappedSet(arr: T[]) {
+ if (map) arr = map(arr);
+ setArr(arr);
+ }
+}
\ No newline at end of file
diff --git a/generator/hooks/use-state-persisted.ts b/generator/hooks/use-state-persisted.ts
new file mode 100644
index 00000000..020af085
--- /dev/null
+++ b/generator/hooks/use-state-persisted.ts
@@ -0,0 +1,38 @@
+import { useState } from 'react';
+
+import { storage } from '../services/helpers';
+
+/**
+ * Creates a state and setter function that persists to localStorage.
+ * @param key The key to use for localStorage.
+ * @param initialValue The initial value to use.
+ */
+export function useStatePersisted(key: string, initialValue: T): [T, (value: T) => void] {
+ if (!process.browser) {
+ return [initialValue as T, () => { }];
+ }
+
+ const [state, setState] = useState(getCached(key, initialValue));
+
+ return [state, setLocalStorageState];
+
+ function setLocalStorageState(value: T | ((value: T) => T)) {
+ if (value instanceof Function) {
+ setState(prev => {
+ const newState = value(prev);
+ return storage.local.set(key, newState);
+ });
+ } else {
+ storage.local.set(key, value);
+ setState(value);
+ }
+ }
+}
+
+function getCached(key: string, initialValue: T) {
+ const cached = storage.local.get(key);
+ if(cached === null && initialValue !== null) {
+ storage.local.set(key, initialValue);
+ }
+ return cached !== null ? cached : initialValue;
+ }
\ No newline at end of file
diff --git a/generator/models/direction.ts b/generator/models/direction.ts
new file mode 100644
index 00000000..fae088e3
--- /dev/null
+++ b/generator/models/direction.ts
@@ -0,0 +1,23 @@
+import { Vector3 } from "three";
+
+export class Direction {
+ constructor (public name: string, public vector: Vector3) { }
+}
+
+export const directions = {
+ UP: new Direction('Up', new Vector3(0, 1, 0)),
+ DOWN: new Direction('Down', new Vector3(0, -1, 0)),
+ LEFT: new Direction('Left', new Vector3(-1, 0, 0)),
+ RIGHT: new Direction('Right', new Vector3(1, 0, 0)),
+ FORWARD: new Direction('Forward', new Vector3(0, 0, 1)),
+ BACK: new Direction('Back', new Vector3(0, 0, -1))
+}
+
+export const directionsList = [
+ directions.UP,
+ directions.DOWN,
+ directions.LEFT,
+ directions.RIGHT,
+ directions.FORWARD,
+ directions.BACK,
+]
\ No newline at end of file
diff --git a/generator/models/planet-mesh.ts b/generator/models/planet-mesh.ts
new file mode 100644
index 00000000..51cb3009
--- /dev/null
+++ b/generator/models/planet-mesh.ts
@@ -0,0 +1,52 @@
+import { MeshPhongMaterial, Geometry, Color, Vector3, VertexColors, MeshLambertMaterial} from 'three';
+import { ShapeGenerator } from './shape-generator';
+import { QuadSphereMesh } from './quad-sphere-mesh';
+import { PlanetLayer } from './planet-settings';
+
+export class PlanetMesh extends QuadSphereMesh {
+ private _radius: number;
+ public get radius() { return this._radius; }
+ public set radius (value: number) { this._radius = Math.max(0, value); }
+ public planetColor: string;
+ private _seed: string;
+ public set seed (value: string) { this._seed = value; }
+ public terrainLayers: PlanetLayer[] = []; // Of interface PlanetLayer, type array
+ public constructor() {
+ super (32, new MeshLambertMaterial({
+ color: '#f0f0f0'
+ }));
+ }
+
+ public regenerateTerrain() {
+ const shapeGenerator = new ShapeGenerator(this.terrainLayers, this.radius, this._seed); // Look at basic arguments for ShapeGenerator class
+ const geometry = this.geometry as Geometry;
+ geometry.vertices = geometry.vertices.map(vertex => shapeGenerator.CalculatePointOnPlanet(vertex));
+ geometry.computeFaceNormals();
+ geometry.computeVertexNormals();
+ geometry.verticesNeedUpdate = true;
+ geometry.normalsNeedUpdate = true;
+ geometry.elementsNeedUpdate = true;
+ this.regenerateWireFrames();
+ }
+
+ public regenerateShading() {
+ const faceMaterial = this.material as MeshPhongMaterial;
+ faceMaterial.vertexColors = VertexColors;
+ faceMaterial.color = new Color('#ffffff'); // new Color (this.settings.color);
+ const center = new Vector3(0, 0, 0);
+ const geometry = this.geometry as Geometry;
+ geometry.faces.forEach(face => {
+ face.vertexColors = [face.a, face.b, face.c].map(i => {
+ const dist = geometry.vertices[i].distanceTo(center) - (this.radius+this.terrainLayers[0].noiseSettings.minValue);
+ if(dist > 0) {
+ // Land
+ return new Color('#008000');
+ } else {
+
+ // Water
+ return new Color('#000080');
+ }
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/generator/models/planet-settings.ts b/generator/models/planet-settings.ts
new file mode 100644
index 00000000..2c6ec67c
--- /dev/null
+++ b/generator/models/planet-settings.ts
@@ -0,0 +1,63 @@
+import { Vector2, Vector3 } from 'three';
+
+export interface PlanetSettings {
+ name: string;
+ seed: string;
+ radius: number; // Size of the planet generated is based on an int/number value defined by the user
+ color: string;
+ terrainLayers: PlanetLayer[]; // Will be made up of PlanetLayer classes
+}
+
+export interface PlanetLayer { // Each layer has these parcreateMountainNoiseameters
+ id?: string; // Can be set by tic ID -> multiple layers from different sectors?
+ name: string;
+ enabled: boolean;
+ maskType: MaskTypes; // Then wrap the image mask around it
+ noiseSettings?: NoiseSettings;
+}
+
+export interface NoiseSettings {
+ baseRoughness: number;
+ roughness: number;
+ persistence: number;
+ octaves: number; // 1+
+ offset: Vector3;
+ minValue: number;
+ strength: number;
+ stretch: Vector2; // 1+
+ skew: Vector3; // 0-1
+}
+
+export enum MaskTypes {
+ None = 'None',
+ FirstLayer = 'First Layer',
+ PrevLayer = 'Previous Layer',
+}
+
+export function createContintentNoise() {
+ return {
+ baseRoughness: 1.5,
+ roughness: 2.5,
+ persistence: 0.3,
+ octaves: 3,
+ offset: new Vector3(0, 0, 0),
+ minValue: -0.05,
+ strength: 0.3,
+ stretch: new Vector2(0.7, 0.7),
+ skew: new Vector3(0, 0, 0)
+ } as NoiseSettings;
+};
+
+export function createMountainNoise() {
+ return {
+ baseRoughness: 1.5,
+ roughness: 2.7,
+ persistence: 0.35,
+ octaves: 6,
+ offset: new Vector3(0, 0, 0),
+ minValue: -0.05,
+ strength: 0.5,
+ stretch: new Vector2(1, 1),
+ skew: new Vector3(0, 0, 0),
+ } as NoiseSettings;
+};
\ No newline at end of file
diff --git a/generator/models/planet.ts b/generator/models/planet.ts
new file mode 100644
index 00000000..436096db
--- /dev/null
+++ b/generator/models/planet.ts
@@ -0,0 +1,42 @@
+import { Group } from "three";
+import { PlanetMesh } from "./planet-mesh";
+import { SeaMesh } from "./sea-mesh";
+
+export class Planet extends Group {
+ public surface: PlanetMesh;
+ public sea: SeaMesh;
+ private _seed: string;
+ public get seed () { return this._seed; }
+ public set seed ( value: string ) {
+ this._seed = value;
+ this.surface.seed = this.sea.seed = this._seed;
+ }
+
+ public rotate: number;
+ constructor () {
+ super();
+ this.add(this.surface = new PlanetMesh());
+ this.add(this.sea = new SeaMesh(this.surface));
+ }
+
+ public regenerateTerrain() {
+ this.surface.regenerateTerrain();
+ this.sea.regenerateTerrain();
+ }
+
+ public regenerateShading() {
+ this.surface.regenerateShading();
+ this.sea.regenerateShading();
+ }
+
+ public regenerateMesh() {
+ this.surface.regenerateMesh();
+ this.sea.regenerateMesh();
+ }
+
+ public update(dT: number) {
+ if (!!this.rotate) {
+ this.rotateY(dT * this.rotate);
+ }
+ }
+}
\ No newline at end of file
diff --git a/generator/models/quad-sphere-mesh.ts b/generator/models/quad-sphere-mesh.ts
new file mode 100644
index 00000000..ec7fee47
--- /dev/null
+++ b/generator/models/quad-sphere-mesh.ts
@@ -0,0 +1,71 @@
+import { Mesh, LineSegments, Material, Geometry, WireframeGeometry, LineBasicMaterial, Color, Vector3, Face3, Vector2 } from 'three';
+import { directionsList, Direction } from './direction';
+
+export class QuadSphereMesh extends Mesh {
+ private _resolution: number;
+ public get resolution(): number { return this._resolution; }
+ public set resolution( value: number ) { this._resolution = Math.max(Math.min(value, 256), 2); }
+ private _wireframes: LineSegments;
+ public get wireframes() { return this._wireframes.visible; }
+ public set wireframes( value: boolean ) { this._wireframes.visible = value; }
+ public constructor (resolution: number = 32, material?: Material ) {
+ super(new Geometry(), material);
+ this.resolution = resolution;
+ this._wireframes = new LineSegments();
+ this._wireframes.visible = false;
+ this.add(this._wireframes);
+ };
+
+ public regenerateMesh () {
+ let geometry = new Geometry();
+ directionsList.forEach(direction => { geometry.merge(this._generateFaceGeometry(direction)); });
+
+ // Merge vertices into a single geometry
+ geometry.mergeVertices();
+ geometry.computeFaceNormals();
+ geometry.computeVertexNormals();
+
+ this.geometry.dispose();
+ this.geometry = geometry;
+ }
+
+ protected regenerateWireFrames () {
+ this._wireframes.geometry.dispose(); // Build the wireframes
+ this._wireframes.geometry = new WireframeGeometry(this.geometry);
+ const wireframeMat = (this._wireframes.material as LineBasicMaterial);
+ wireframeMat.color = new Color(0x000000);
+ wireframeMat.linewidth = 2;
+ wireframeMat.opacity = 0.25;
+ wireframeMat.transparent = true;
+ }
+
+ private _generateFaceGeometry(localUp: Direction) {
+ const axisA = new Vector3(localUp.vector.y, localUp.vector.z, localUp.vector.x);
+ const axisB = localUp.vector.clone().cross(axisA);
+ const geometry = new Geometry();
+ const vertices: Vector3[] = [];
+ const triangles: Face3[] = [];
+
+ for (let y = 0; y < this.resolution; y++) {
+ for (let x = 0; x < this.resolution; x++) {
+ const i = x + y * this.resolution;
+ const percent = new Vector2(x, y).divideScalar(this.resolution - 1);
+ const pointOnUnitCube = localUp.vector.clone()
+ .add(axisA.clone().multiplyScalar((percent.x - 0.5) * 2))
+ .add(axisB.clone().multiplyScalar((percent.y - 0.5) * 2));
+ vertices[i] = pointOnUnitCube.clone().normalize();
+
+ if (x != this.resolution - 1 && y != this.resolution - 1) {
+ triangles.push(
+ new Face3(i, i + this.resolution + 1, i + this.resolution),
+ new Face3(i, i + 1, i + this.resolution + 1)
+ );
+ }
+ }
+ }
+
+ geometry.vertices = vertices;
+ geometry.faces = triangles;
+ return geometry;
+ }
+}
\ No newline at end of file
diff --git a/generator/models/sea-mesh.ts b/generator/models/sea-mesh.ts
new file mode 100644
index 00000000..a7ad7020
--- /dev/null
+++ b/generator/models/sea-mesh.ts
@@ -0,0 +1,52 @@
+import { MeshPhongMaterial, Geometry, Color, Vector3, VertexColors} from 'three';
+import { QuadSphereMesh } from './quad-sphere-mesh';
+import { PlanetMesh } from './planet-mesh';
+
+export class SeaMesh extends QuadSphereMesh {
+ private _radius: number = 1;
+ public get radius () { return this._radius }
+ public set radius ( value: number ) { this._radius = Math.max(0, value); }
+ public get opacity () { return ( this.material as MeshPhongMaterial ).opacity; }
+ public set opacity ( value: number ) { ( this.material as MeshPhongMaterial ).opacity = value; }
+
+ public color: string = '#002050';
+ public seed: string;
+
+ private _planet: PlanetMesh;
+ public constructor (planet: PlanetMesh) {
+ super ( 32, new MeshPhongMaterial ({
+ transparent: true,
+ color: '#000810',
+ opacity: 0.85,
+ specular: '#004488',
+ emissive: '#001030',
+ flatShading: true,
+ }));
+
+ this._planet = planet;
+ }
+
+ public regenerateTerrain() {
+ const geometry = this.geometry as Geometry;
+ geometry.vertices = geometry.vertices.map( vertex => vertex.normalize().multiplyScalar(( this._planet.radius + this._radius - 1 )));
+ geometry.computeFaceNormals();
+ geometry.computeVertexNormals();
+ geometry.verticesNeedUpdate = true;
+ geometry.normalsNeedUpdate = true;
+ geometry.elementsNeedUpdate = true;
+ this.regenerateWireFrames();
+ }
+
+ public regenerateShading() {
+ const faceMaterial = this.material as MeshPhongMaterial;
+ faceMaterial.vertexColors = VertexColors; // facematerial.color = new Color('#ffffff'); // new Color(this.settings.color);
+ const center = new Vector3(0, 0, 0);
+ const geometry = this.geometry as Geometry; // geometry.faces.forEach(face => { face.vertexColors = [face.a, face.b, face.c].map(i => new Color('#000080')); })
+ }
+
+ public update(dT: number) {
+ /* if (!!this.rotate) {
+ this.rotateY(dT*this.rotate);
+ } */
+ }
+}
\ No newline at end of file
diff --git a/generator/models/shape-generator.ts b/generator/models/shape-generator.ts
new file mode 100644
index 00000000..c5b521c1
--- /dev/null
+++ b/generator/models/shape-generator.ts
@@ -0,0 +1,79 @@
+
+import { Vector3, Quaternion } from 'three';
+import Alea from 'alea';
+import SimplexNoise from 'simplex-noise'; // We had import errors with the latest Simplex version (^4.0.1), so we've gone backwards for now
+
+import { PlanetLayer, NoiseSettings, MaskTypes } from './planet-settings';
+
+export class ShapeGenerator {
+ private _noiseFilters: NoiseFilter[];
+ private _layers: PlanetLayer[];
+ private _radius: number;
+
+ public constructor(layers: PlanetLayer[], radius: number, seed: string) {
+ this._layers = layers;
+ this._radius = radius;
+
+ const prng = Alea(seed || '');
+ this._noiseFilters = [];
+ for (let i = 0; i < this._layers.length; i++) {
+ this._noiseFilters[i] = new NoiseFilter(new SimplexNoise(prng), this._layers[i].noiseSettings);
+ }
+ }
+
+ public CalculatePointOnPlanet(pointOnSphere: Vector3): Vector3 {
+ let firstLayerValue = 0;
+ let prevLayerValue = 0;
+ let elevation = -1;
+
+ pointOnSphere.normalize();
+ const pointOnUnitSphere: Vector3 = pointOnSphere.clone();
+
+ if (this._noiseFilters.length > 0) {
+ firstLayerValue = prevLayerValue = this._noiseFilters[0].Evaluate(pointOnUnitSphere);
+ if (this._layers[0].enabled) {
+ elevation = firstLayerValue;
+ }
+ }
+
+ for (let i = 1; i < this._noiseFilters.length; i++) {
+ if (this._layers[i].enabled) {
+ const mask = (this._layers[i].maskType === MaskTypes.FirstLayer && firstLayerValue > this._layers[0].noiseSettings.minValue) ? 1 :
+ (this._layers[i].maskType === MaskTypes.PrevLayer && prevLayerValue > this._layers[i-1].noiseSettings.minValue) ? 1 :
+ (this._layers[i].maskType === MaskTypes.None) ? 1 : 0;
+
+ prevLayerValue = this._noiseFilters[i].Evaluate(pointOnUnitSphere);
+ elevation = Math.max(elevation, prevLayerValue * mask);
+ }
+ }
+
+ return pointOnSphere.multiplyScalar(this._radius + elevation);
+ }
+}
+
+export class NoiseFilter {
+
+ public constructor(private noise: SimplexNoise, private settings: NoiseSettings) { }
+
+ public Evaluate(point: Vector3): number {
+ let noiseValue = 0;
+ let frequency = this.settings.baseRoughness;
+ let amplitude = 1;
+ let ampTotal = amplitude;
+
+ let q = new Quaternion().setFromAxisAngle(this.settings.skew, Math.PI / 2);
+ for (let i = 0; i < this.settings.octaves; i++) {
+ let p = point.clone().multiplyScalar(frequency).add(this.settings.offset);
+ p = p.applyQuaternion(q);
+ let v = (this.noise.noise3D(p.x/this.settings.stretch.x, p.y/this.settings.stretch.y, p.z/this.settings.stretch.x));
+ noiseValue += v * amplitude;
+ frequency *= this.settings.roughness;
+ amplitude *= this.settings.persistence;
+ ampTotal += amplitude;
+ }
+
+ noiseValue = noiseValue / ampTotal;
+ noiseValue = Math.max(noiseValue, this.settings.minValue);
+ return noiseValue * this.settings.strength;
+ }
+}
\ No newline at end of file
diff --git a/generator/services/base-scene-manager.ts b/generator/services/base-scene-manager.ts
new file mode 100644
index 00000000..0ff8be9c
--- /dev/null
+++ b/generator/services/base-scene-manager.ts
@@ -0,0 +1,7 @@
+export default interface SceneManager {
+ scene: THREE.Scene;
+ camera: THREE.Camera;
+ init(canvas: HTMLCanvasElement): void;
+ start(): void;
+ stop(): void;
+}
\ No newline at end of file
diff --git a/generator/services/event-emitter.ts b/generator/services/event-emitter.ts
new file mode 100644
index 00000000..5cadfc55
--- /dev/null
+++ b/generator/services/event-emitter.ts
@@ -0,0 +1,37 @@
+export type EventHandler = (arg: any) => void;
+
+export class EventEmitter {
+ private _events: { [key: string]: EventHandler[] } = {};
+
+ public bind(event: string, handler: EventHandler) {
+ this._events[event] = this._events[event] || [];
+ this._events[event].push(handler);
+ }
+
+ public once(event: string, handler: EventHandler) {
+ this.bind(event, function h (args) {
+ this.unbind(event, h);
+ handler.call(null, args);
+ });
+ }
+
+ public unbind(event: string, handler: EventHandler) {
+ if (!this._events[event]) return;
+
+ let index = this._events[event].indexOf(handler);
+ if (index === -1) return;
+
+ this._events[event].splice(index, 1);
+ }
+
+ public emit(event: string, args: any) {
+ if (!this._events[event]) return;
+
+ let handlers = this._events[event].slice();
+ let count = handlers.length;
+
+ for (let i = 0; i < count; i++) {
+ handlers[i].call(null, args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/generator/services/helpers.ts b/generator/services/helpers.ts
new file mode 100644
index 00000000..12e15b5e
--- /dev/null
+++ b/generator/services/helpers.ts
@@ -0,0 +1,42 @@
+
+export const EMPTY_STRING = '';
+
+export function guid(): string {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+export function randomSeed(chunks: number = 2) {
+ return Array(chunks).fill(0).map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36).toUpperCase()).join('');
+}
+
+export class StorageAdapter {
+ private _storage: Storage;
+
+ constructor(storage: 'localStorage'|'sessionStorage') {
+ this._storage = process.browser ? window[storage] : null;
+ }
+
+ public get(key: string, defaultValue: T = null): T {
+ if (!this._storage) return defaultValue;
+ try {
+ const item = this._storage.getItem(key);
+ return item ? JSON.parse(item) : defaultValue;
+ } catch (error) {
+ console.log(error);
+ return defaultValue;
+ }
+ }
+
+ public set(key: string, value: T): T {
+ if(this._storage) this._storage.setItem(key, JSON.stringify(value));
+ return value;
+ }
+}
+
+export const storage = {
+ local: new StorageAdapter('localStorage'),
+ session: new StorageAdapter('sessionStorage')
+};
\ No newline at end of file
diff --git a/generator/services/orbit-controls.ts b/generator/services/orbit-controls.ts
new file mode 100644
index 00000000..b97a0a9e
--- /dev/null
+++ b/generator/services/orbit-controls.ts
@@ -0,0 +1,697 @@
+import { Camera, Vector2, Vector3, Matrix4, EventDispatcher, MOUSE, OrthographicCamera, PerspectiveCamera, Quaternion, Spherical } from 'three';
+
+const STATE = {
+ NONE: - 1,
+ ROTATE: 0,
+ DOLLY: 1,
+ PAN: 2,
+ TOUCH_ROTATE: 3,
+ TOUCH_DOLLY: 4,
+ TOUCH_PAN: 5
+};
+
+const CHANGE_EVENT = { type: 'change' };
+const START_EVENT = { type: 'start' };
+const END_EVENT = { type: 'end' };
+const EPS = 0.000001;
+/*
+*
+* This set of controls performs orbiting, dollying (zooming), and panning.
+* Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+* Orbit - left mouse / touch: one finger move
+* Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
+* Pan - right mouse, or arrow keys / touch: three finger swipe
+*/
+export class OrbitControls extends EventDispatcher {
+ object: Camera;
+ domElement: HTMLElement | HTMLDocument;
+ window: Window;
+
+ // API
+ enabled: boolean;
+ target: Vector3;
+
+ enableZoom: boolean;
+ zoomSpeed: number;
+ minDistance: number;
+ maxDistance: number;
+ enableRotate: boolean;
+ rotateSpeed: number;
+ enablePan: boolean;
+ keyPanSpeed: number;
+ autoRotate: boolean;
+ autoRotateSpeed: number;
+ minZoom: number;
+ maxZoom: number;
+ minPolarAngle: number;
+ maxPolarAngle: number;
+ minAzimuthAngle: number;
+ maxAzimuthAngle: number;
+ enableKeys: boolean;
+ keys: { LEFT: number; UP: number; RIGHT: number; BOTTOM: number; };
+ mouseButtons: { ORBIT: MOUSE; ZOOM: MOUSE; PAN: MOUSE; };
+ enableDamping: boolean;
+ dampingFactor: number;
+
+ private spherical: Spherical;
+ private sphericalDelta: Spherical;
+ private scale: number;
+ private target0: Vector3;
+ private position0: Vector3;
+ private zoom0: any;
+ private state: number;
+ private panOffset: Vector3;
+ private zoomChanged: boolean;
+
+ private rotateStart: Vector2;
+ private rotateEnd: Vector2;
+ private rotateDelta: Vector2
+
+ private panStart: Vector2;
+ private panEnd: Vector2;
+ private panDelta: Vector2;
+
+ private dollyStart: Vector2;
+ private dollyEnd: Vector2;
+ private dollyDelta: Vector2;
+
+ private updateLastPosition: Vector3;
+ private updateOffset: Vector3;
+ private updateQuat: Quaternion;
+ private updateLastQuaternion: Quaternion;
+ private updateQuatInverse: Quaternion;
+
+ private panLeftV: Vector3;
+ private panUpV: Vector3;
+ private panInternalOffset: Vector3;
+
+ private onContextMenu: EventListener;
+ private onMouseUp: EventListener;
+ private onMouseDown: EventListener;
+ private onMouseMove: EventListener;
+ private onMouseWheel: EventListener;
+ private onTouchStart: EventListener;
+ private onTouchEnd: EventListener;
+ private onTouchMove: EventListener;
+ private onKeyDown: EventListener;
+
+ constructor (object: Camera, domElement?: HTMLElement, domWindow?: Window) {
+ super();
+ this.object = object;
+
+ this.domElement = ( domElement !== undefined ) ? domElement : document;
+ this.window = ( domWindow !== undefined ) ? domWindow : window;
+
+ // Set to false to disable this control
+ this.enabled = true;
+
+ // "target" sets the location of focus, where the object orbits around
+ this.target = new Vector3();
+
+ // How far you can dolly in and out ( PerspectiveCamera only )
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ // How far you can zoom in and out ( OrthographicCamera only )
+ this.minZoom = 0;
+ this.maxZoom = Infinity;
+
+ // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+ this.minPolarAngle = 0; // radians
+ this.maxPolarAngle = Math.PI; // radians
+
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+ this.minAzimuthAngle = - Infinity; // radians
+ this.maxAzimuthAngle = Infinity; // radians
+
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+ this.enableDamping = false;
+ this.dampingFactor = 0.25;
+
+ // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0;
+
+ // Set to false to disable rotating
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0;
+
+ // Set to false to disable panning
+ this.enablePan = true;
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+
+ // Set to false to disable use of the keys
+ this.enableKeys = true;
+
+ // The four arrow keys
+ this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+ // Mouse buttons
+ this.mouseButtons = { ORBIT: MOUSE.LEFT, ZOOM: MOUSE.MIDDLE, PAN: MOUSE.RIGHT };
+
+ // for reset
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = (this.object as any).zoom;
+
+ // for update speedup
+ this.updateOffset = new Vector3();
+ // so camera.up is the orbit axis
+ this.updateQuat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
+ this.updateQuatInverse = this.updateQuat.clone().inverse();
+ this.updateLastPosition = new Vector3();
+ this.updateLastQuaternion = new Quaternion();
+
+ this.state = STATE.NONE;
+ this.scale = 1;
+
+ // current position in spherical coordinates
+ this.spherical = new Spherical();
+ this.sphericalDelta = new Spherical();
+
+ this.panOffset = new Vector3();
+ this.zoomChanged = false;
+
+ this.rotateStart = new Vector2();
+ this.rotateEnd = new Vector2();
+ this.rotateDelta = new Vector2();
+
+ this.panStart = new Vector2();
+ this.panEnd = new Vector2();
+ this.panDelta = new Vector2();
+
+ this.dollyStart = new Vector2();
+ this.dollyEnd = new Vector2();
+ this.dollyDelta = new Vector2();
+
+ this.panLeftV = new Vector3();
+ this.panUpV = new Vector3();
+ this.panInternalOffset = new Vector3();
+
+ // event handlers - FSM: listen for events and reset state
+
+ this.onMouseDown = ( event: ThreeEvent ) => {
+ if ( this.enabled === false ) return;
+ event.preventDefault();
+ if ( (event as any).button === this.mouseButtons.ORBIT ) {
+ if ( this.enableRotate === false ) return;
+ this.rotateStart.set( event.clientX, event.clientY );
+ this.state = STATE.ROTATE;
+ } else if ( event.button === this.mouseButtons.ZOOM ) {
+ if ( this.enableZoom === false ) return;
+ this.dollyStart.set( event.clientX, event.clientY );
+ this.state = STATE.DOLLY;
+ } else if ( event.button === this.mouseButtons.PAN ) {
+ if ( this.enablePan === false ) return;
+ this.panStart.set( event.clientX, event.clientY );
+ this.state = STATE.PAN;
+ }
+
+ if ( this.state !== STATE.NONE ) {
+ document.addEventListener( 'mousemove', this.onMouseMove, false );
+ document.addEventListener( 'mouseup', this.onMouseUp, false );
+ this.dispatchEvent( START_EVENT );
+ }
+ };
+
+ this.onMouseMove = ( event: ThreeEvent ) => {
+
+ if ( this.enabled === false ) return;
+
+ event.preventDefault();
+
+ if ( this.state === STATE.ROTATE ) {
+ if ( this.enableRotate === false ) return;
+ this.rotateEnd.set( event.clientX, event.clientY );
+ this.rotateDelta.subVectors( this.rotateEnd, this.rotateStart );
+ const element = this.domElement === document ? this.domElement.body : this.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ this.rotateLeft( 2 * Math.PI * this.rotateDelta.x / (element as any).clientWidth * this.rotateSpeed );
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ this.rotateUp( 2 * Math.PI * this.rotateDelta.y / (element as any).clientHeight * this.rotateSpeed );
+ this.rotateStart.copy( this.rotateEnd );
+
+ this.update();
+ } else if ( this.state === STATE.DOLLY ) {
+
+ if ( this.enableZoom === false ) return;
+
+ this.dollyEnd.set( event.clientX, event.clientY );
+ this.dollyDelta.subVectors( this.dollyEnd, this.dollyStart );
+
+ if ( this.dollyDelta.y > 0 ) {
+ this.dollyIn( this.getZoomScale() );
+ } else if ( this.dollyDelta.y < 0 ) {
+ this.dollyOut( this.getZoomScale() );
+ }
+
+ this.dollyStart.copy( this.dollyEnd );
+ this.update();
+ } else if ( this.state === STATE.PAN ) {
+
+ if ( this.enablePan === false ) return;
+
+ this.panEnd.set( event.clientX, event.clientY );
+ this.panDelta.subVectors( this.panEnd, this.panStart );
+ this.pan( this.panDelta.x, this.panDelta.y );
+ this.panStart.copy( this.panEnd );
+ this.update();
+ }
+ }
+
+ this.onMouseUp = ( event: ThreeEvent ) => {
+ if ( this.enabled === false ) return;
+ document.removeEventListener( 'mousemove', this.onMouseMove, false );
+ document.removeEventListener( 'mouseup', this.onMouseUp, false );
+
+ this.dispatchEvent( END_EVENT );
+ this.state = STATE.NONE;
+ };
+
+ this.onMouseWheel = ( event: ThreeEvent ) => {
+
+ if ( this.enabled === false || this.enableZoom === false || ( this.state !== STATE.NONE && this.state !== STATE.ROTATE ) ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ( event.deltaY < 0 ) {
+ this.dollyOut( this.getZoomScale() );
+ } else if ( event.deltaY > 0 ) {
+ this.dollyIn( this.getZoomScale() );
+ }
+
+ this.update();
+
+ this.dispatchEvent( START_EVENT ); // not sure why these are here...
+ this.dispatchEvent( END_EVENT );
+ };
+
+ this.onKeyDown = ( event: ThreeEvent ) => {
+
+ if ( this.enabled === false || this.enableKeys === false || this.enablePan === false ) return;
+
+ switch ( event.keyCode ) {
+ case this.keys.UP: {
+ this.pan( 0, this.keyPanSpeed );
+ this.update();
+ } break;
+ case this.keys.BOTTOM: {
+ this.pan( 0, - this.keyPanSpeed );
+ this.update();
+ } break;
+ case this.keys.LEFT: {
+ this.pan( this.keyPanSpeed, 0 );
+ this.update();
+ } break;
+ case this.keys.RIGHT: {
+ this.pan( - this.keyPanSpeed, 0 );
+ this.update();
+ } break;
+ }
+ };
+
+ this.onTouchStart = ( event: ThreeEvent ) => {
+
+ if ( this.enabled === false ) return;
+
+ switch ( event.touches.length ) {
+ // one-fingered touch: rotate
+ case 1: {
+ if ( this.enableRotate === false ) return;
+
+ this.rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+ this.state = STATE.TOUCH_ROTATE;
+ } break;
+ // two-fingered touch: dolly
+ case 2: {
+ if ( this.enableZoom === false ) return;
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+ this.dollyStart.set( 0, distance );
+ this.state = STATE.TOUCH_DOLLY;
+ } break;
+ // three-fingered touch: pan
+ case 3: {
+ if ( this.enablePan === false ) return;
+
+ this.panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+ this.state = STATE.TOUCH_PAN;
+ } break;
+ default: {
+ this.state = STATE.NONE;
+ }
+ }
+
+ if ( this.state !== STATE.NONE ) {
+ this.dispatchEvent( START_EVENT );
+ }
+ };
+
+ this.onTouchMove = ( event: ThreeEvent ) => {
+
+ if ( this.enabled === false ) return;
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch ( event.touches.length ) {
+ // one-fingered touch: rotate
+ case 1: {
+ if ( this.enableRotate === false ) return;
+ if ( this.state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
+
+ this.rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+ this.rotateDelta.subVectors( this.rotateEnd, this.rotateStart );
+
+ var element = this.domElement === document ? this.domElement.body : this.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ this.rotateLeft( 2 * Math.PI * this.rotateDelta.x / (element as any).clientWidth * this.rotateSpeed );
+
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ this.rotateUp( 2 * Math.PI * this.rotateDelta.y / (element as any).clientHeight * this.rotateSpeed );
+
+ this.rotateStart.copy( this.rotateEnd );
+
+ this.update();
+ } break;
+ // two-fingered touch: dolly
+ case 2: {
+ if ( this.enableZoom === false ) return;
+ if ( this.state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
+
+ //console.log( 'handleTouchMoveDolly' );
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ this.dollyEnd.set( 0, distance );
+
+ this.dollyDelta.subVectors( this.dollyEnd, this.dollyStart );
+
+ if ( this.dollyDelta.y > 0 ) {
+ this.dollyOut( this.getZoomScale() );
+ } else if ( this.dollyDelta.y < 0 ) {
+ this.dollyIn( this.getZoomScale() );
+ }
+
+ this.dollyStart.copy( this.dollyEnd );
+ this.update();
+ } break;
+ // three-fingered touch: pan
+ case 3: {
+ if ( this.enablePan === false ) return;
+ if ( this.state !== STATE.TOUCH_PAN ) return; // is this needed?...
+ this.panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+ this.panDelta.subVectors( this.panEnd, this.panStart );
+ this.pan( this.panDelta.x, this.panDelta.y );
+ this.panStart.copy( this.panEnd );
+ this.update();
+ } break;
+ default: {
+ this.state = STATE.NONE;
+ }
+ }
+ };
+
+ this.onTouchEnd = ( event: Event ) => {
+
+ if ( this.enabled === false ) return;
+ this.dispatchEvent( END_EVENT );
+ this.state = STATE.NONE;
+ }
+
+ this.onContextMenu = (event) => {
+ event.preventDefault();
+ };
+
+ this.domElement.addEventListener( 'contextmenu', this.onContextMenu, false );
+
+ this.domElement.addEventListener( 'mousedown', this.onMouseDown, false );
+ this.domElement.addEventListener( 'wheel', this.onMouseWheel, false );
+
+ this.domElement.addEventListener( 'touchstart', this.onTouchStart, false );
+ this.domElement.addEventListener( 'touchend', this.onTouchEnd, false );
+ this.domElement.addEventListener( 'touchmove', this.onTouchMove, false );
+
+ this.window.addEventListener( 'keydown', this.onKeyDown, false );
+
+ // force an update at start
+ this.update();
+ }
+
+ update () {
+ const position = this.object.position;
+ this.updateOffset.copy( position ).sub( this.target );
+
+ // rotate offset to "y-axis-is-up" space
+ this.updateOffset.applyQuaternion( this.updateQuat );
+
+ // angle from z-axis around y-axis
+ this.spherical.setFromVector3( this.updateOffset );
+
+ if ( this.autoRotate && this.state === STATE.NONE ) {
+ this.rotateLeft( this.getAutoRotationAngle() );
+ }
+
+ (this.spherical as any).theta += (this.sphericalDelta as any).theta;
+ (this.spherical as any).phi += (this.sphericalDelta as any).phi;
+
+ // restrict theta to be between desired limits
+ (this.spherical as (any) as any).theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, (this.spherical as any).theta ) );
+
+ // restrict phi to be between desired limits
+ (this.spherical as any).phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, (this.spherical as any).phi ) );
+
+ this.spherical.makeSafe();
+
+ (this.spherical as any).radius *= this.scale;
+
+ // restrict radius to be between desired limits
+ (this.spherical as any).radius = Math.max( this.minDistance, Math.min( this.maxDistance, (this.spherical as any).radius ) );
+
+ // move target to panned location
+ this.target.add( this.panOffset );
+
+ this.updateOffset.setFromSpherical( this.spherical );
+
+ // rotate offset back to "camera-up-vector-is-up" space
+ this.updateOffset.applyQuaternion( this.updateQuatInverse );
+
+ position.copy( this.target ).add( this.updateOffset );
+
+ this.object.lookAt( this.target );
+
+ if ( this.enableDamping === true ) {
+
+ (this.sphericalDelta as any).theta *= ( 1 - this.dampingFactor );
+ (this.sphericalDelta as any).phi *= ( 1 - this.dampingFactor );
+
+ } else {
+
+ this.sphericalDelta.set( 0, 0, 0 );
+
+ }
+
+ this.scale = 1;
+ this.panOffset.set( 0, 0, 0 );
+
+ // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if ( this.zoomChanged ||
+ this.updateLastPosition.distanceToSquared( this.object.position ) > EPS ||
+ 8 * ( 1 - this.updateLastQuaternion.dot( this.object.quaternion ) ) > EPS ) {
+
+ this.dispatchEvent( CHANGE_EVENT );
+ this.updateLastPosition.copy( this.object.position );
+ this.updateLastQuaternion.copy( this.object.quaternion );
+ this.zoomChanged = false;
+ return true;
+ }
+ return false;
+ }
+
+ panLeft( distance: number, objectMatrix: Matrix4 ) {
+ this.panLeftV.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+ this.panLeftV.multiplyScalar( - distance );
+ this.panOffset.add( this.panLeftV );
+ }
+
+ panUp( distance: number, objectMatrix: Matrix4 ) {
+ this.panUpV.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
+ this.panUpV.multiplyScalar( distance );
+ this.panOffset.add( this.panUpV );
+ }
+
+ // deltaX and deltaY are in pixels; right and down are positive
+ pan( deltaX: number, deltaY: number ) {
+ const element = this.domElement === document ? this.domElement.body : this.domElement;
+
+ if (this._checkPerspectiveCamera(this.object)) {
+ // perspective
+ const position = this.object.position;
+ this.panInternalOffset.copy( position ).sub( this.target );
+ var targetDistance = this.panInternalOffset.length();
+
+ // half of the fov is center to top of screen
+ targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );
+
+ // we actually don't use screenWidth, since perspective camera is fixed to screen height
+ this.panLeft( 2 * deltaX * targetDistance / (element as any).clientHeight, this.object.matrix );
+ this.panUp( 2 * deltaY * targetDistance / (element as any).clientHeight, this.object.matrix );
+ } else if (this._checkOrthographicCamera(this.object)) {
+ // orthographic
+ this.panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / (element as any).clientWidth, this.object.matrix );
+ this.panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / (element as any).clientHeight, this.object.matrix );
+ } else {
+ // camera neither orthographic nor perspective
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+ this.enablePan = false;
+ }
+ }
+
+ dollyIn( dollyScale: number ) {
+ if (this._checkPerspectiveCamera(this.object)) {
+ this.scale /= dollyScale;
+ } else if (this._checkOrthographicCamera(this.object)) {
+ this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
+ this.object.updateProjectionMatrix();
+ this.zoomChanged = true;
+ } else {
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ this.enableZoom = false;
+ }
+ }
+
+ dollyOut( dollyScale: number ) {
+ if (this._checkPerspectiveCamera(this.object)) {
+ this.scale *= dollyScale;
+ } else if (this._checkOrthographicCamera(this.object)) {
+ this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
+ this.object.updateProjectionMatrix();
+ this.zoomChanged = true;
+ } else {
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ this.enableZoom = false;
+ }
+ }
+
+ getAutoRotationAngle() {
+ return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed;
+ }
+
+ getZoomScale() {
+ return Math.pow( 0.95, this.zoomSpeed );
+ }
+
+ rotateLeft( angle: number ) {
+ (this.sphericalDelta as any).theta -= angle;
+ }
+
+ rotateUp( angle: number ) {
+ (this.sphericalDelta as any).phi -= angle;
+ }
+
+ getPolarAngle (): number {
+ return (this.spherical as any).phi;
+ }
+
+ getAzimuthalAngle (): number {
+ return (this.spherical as any).theta;
+ }
+
+ dispose (): void {
+ this.domElement.removeEventListener( 'contextmenu', this.onContextMenu, false );
+ this.domElement.removeEventListener( 'mousedown', this.onMouseDown, false );
+ this.domElement.removeEventListener( 'wheel', this.onMouseWheel, false );
+
+ this.domElement.removeEventListener( 'touchstart', this.onTouchStart, false );
+ this.domElement.removeEventListener( 'touchend', this.onTouchEnd, false );
+ this.domElement.removeEventListener( 'touchmove', this.onTouchMove, false );
+
+ document.removeEventListener( 'mousemove', this.onMouseMove, false );
+ document.removeEventListener( 'mouseup', this.onMouseUp, false );
+
+ this.window.removeEventListener( 'keydown', this.onKeyDown, false );
+ //this.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+ }
+
+ reset (): void {
+ this.target.copy( this.target0 );
+ this.object.position.copy( this.position0 );
+ (this.object as any).zoom = this.zoom0;
+
+ (this.object as any).updateProjectionMatrix();
+ this.dispatchEvent( CHANGE_EVENT );
+
+ this.update();
+
+ this.state = STATE.NONE;
+ }
+
+ saveState(): void {
+ this.target0.copy(this.target);
+ this.position0.copy(this.object.position);
+ // Check whether the camera has zoom property
+ if (this._checkOrthographicCamera(this.object) || this._checkPerspectiveCamera(this.object)){
+ this.zoom0 = this.object.zoom;
+ }
+ }
+
+ // backward compatibility
+ get center(): Vector3 {
+ console.warn('OrbitControls: .center has been renamed to .target');
+ return this.target;
+ }
+ get noZoom(): boolean {
+ console.warn( 'OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ return ! this.enableZoom;
+ }
+
+ set noZoom( value: boolean ) {
+ console.warn( 'OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
+ this.enableZoom = ! value;
+ }
+
+ /**
+ * TS typeguard. Checks whether the provided camera is PerspectiveCamera.
+ * If the check passes (returns true) the passed camera will have the type PerspectiveCamera in the if branch where the check was performed.
+ * @param camera Object to be checked.
+ */
+ private _checkPerspectiveCamera(camera: Camera): camera is PerspectiveCamera{
+ return (camera as PerspectiveCamera).isPerspectiveCamera;
+ }
+ /**
+ * TS typeguard. Checks whether the provided camera is OrthographicCamera.
+ * If the check passes (returns true) the passed camera will have the type OrthographicCamera in the if branch where the check was performed.
+ * @param camera Object to be checked.
+ */
+ private _checkOrthographicCamera(camera: Camera): camera is OrthographicCamera{
+ return (camera as OrthographicCamera).isOrthographicCamera;
+ }
+}
+
+interface ThreeEvent extends Event {
+ clientX: number;
+ clientY: number;
+ deltaY: number;
+ button: MOUSE;
+ touches: Array;
+ keyCode: number;
+}
\ No newline at end of file
diff --git a/generator/services/planet-editor-scene.ts b/generator/services/planet-editor-scene.ts
new file mode 100644
index 00000000..6e7249f6
--- /dev/null
+++ b/generator/services/planet-editor-scene.ts
@@ -0,0 +1,69 @@
+import * as THREE from 'three';
+import { OrbitControls } from './orbit-controls';
+
+import SceneManager from './base-scene-manager';
+import { Planet } from '../models/planet';
+
+export default class PlanetEditorSceneManager implements SceneManager {
+
+ public planet: Planet;
+ public scene: THREE.Scene;
+ public camera: THREE.Camera;
+ public controls: OrbitControls;
+
+ private _renderer: THREE.WebGLRenderer;
+ private _handleAnimationFrame: (deltaT: number) => void;
+ private _prevT: number = 0;
+
+ constructor() {
+ this.scene = new THREE.Scene();
+ this.scene.background = new THREE.Color('#000000');
+
+ this.planet = new Planet();
+ this.scene.add(this.planet);
+
+ const ambientLight = new THREE.AmbientLight('#ffffff', 0.15)
+ this.scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight('#efe8e9', 0.8)
+ directionalLight.position.set(-1000, 0, 1000)
+ directionalLight.target = this.planet;
+ this.scene.add(directionalLight);
+
+ this._handleAnimationFrame = (t) => {
+ const dT = (t - this._prevT) / 1000;
+ this._prevT = t;
+ this.planet.update(dT);
+ this.controls.update();
+
+ this._renderer.render(this.scene, this.camera);
+ };
+ }
+
+ public init(canvas: HTMLCanvasElement) {
+ canvas.height = 1080;
+ canvas.width = canvas.height;// * (16 / 9);
+
+ this._renderer = new THREE.WebGLRenderer({
+ canvas,
+ antialias: true
+ });
+ this._renderer.setPixelRatio(window.devicePixelRatio);
+
+ this.camera = new THREE.PerspectiveCamera(60, canvas.width / canvas.height, 0.1, 1000);
+
+ this.controls = new OrbitControls(this.camera, this._renderer.domElement);
+
+ this.camera.position.set(0, 0, 5);
+ this.camera.lookAt(0, 0, 0);
+ this.controls.update();
+ }
+
+ public start() {
+ this._renderer.setAnimationLoop(this._handleAnimationFrame.bind(this));
+ }
+
+ public stop() {
+ this._renderer.setAnimationLoop(null);
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index abfd8d9c..642325d9 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@emotion/styled": "^11",
"@headlessui/react": "^1.7.7",
"@lens-protocol/react": "^0.1.1",
+ "@primer/octicons-react": "9.1.1",
"@reach/portal": "^0.18.0",
"@supabase/auth-helpers-nextjs": "^0.5.2",
"@supabase/auth-helpers-react": "^0.3.1",
@@ -27,7 +28,14 @@
"@thirdweb-dev/react": "^3.6.8",
"@thirdweb-dev/sdk": "^3.6.8",
"@thirdweb-dev/storage": "^1.0.6",
+ "@types/rc-slider": "^9.3.1",
+ "@types/rc-tooltip": "^3.7.7",
+ "@types/react-color": "^3.0.6",
+ "@types/react-dom": "^18.0.10",
+ "alea": "^1.0.1",
+ "bootstrap": "^5.2.3",
"classnames": "^2.3.2",
+ "cross-env": "^7.0.3",
"cuid": "^2.1.8",
"dayjs": "^1.11.7",
"ethers": "^5.7.2",
@@ -35,19 +43,25 @@
"graphql": "^16.6.0",
"moralis": "^2.10.3",
"moralis-v1": "^1.12.0",
- "next": "13.1.0",
+ "next": "^13.1.4",
"next-themes": "^0.2.1",
"omit-deep": "^0.3.0",
"performant-array-to-tree": "^1.11.0",
"preact": "^10.11.3",
- "react": "18.2.0",
+ "rc-slider": "^10.1.0",
+ "rc-tooltip": "^5.3.1",
+ "react": "^18.2.0",
+ "react-bootstrap": "1.0.0-beta.12",
+ "react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-hook-form": "^7.41.3",
"react-icons": "^4.7.1",
"react-markdown": "^8.0.4",
"react-moralis": "^1.4.2",
"react-syntax-highlighter": "^15.5.0",
+ "simplex-noise": "2.4.0",
"swr": "^2.0.0",
+ "three": "0.108.0",
"uuid": "^9.0.0",
"uuidv4": "^6.2.13",
"web3uikit": "^1.0.4"
@@ -65,8 +79,9 @@
"@types/cookie": "^0.5.1",
"@types/node": "^18.11.18",
"@types/omit-deep": "^0.3.0",
- "@types/react": "^18.0.26",
+ "@types/react": "^18.0.27",
"@types/react-syntax-highlighter": "^15.5.5",
+ "@types/three": "^0.148.0",
"@types/unist": "^2.0.6",
"auto-prefixer": "^0.4.2",
"autoprefixer": "^10.4.13",
diff --git a/pages/.DS_Store b/pages/.DS_Store
index f14418e4..d882e7d7 100644
Binary files a/pages/.DS_Store and b/pages/.DS_Store differ
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 670020b2..6bcadde6 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -11,6 +11,11 @@ import { useState } from "react";
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs';
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react';
+// For threejs generator components
+import 'bootstrap/dist/css/bootstrap.min.css';
+import 'rc-slider/assets/index.css';
+import 'rc-tooltip/assets/bootstrap.css';
+
function MyApp({ Component, pageProps }: AppProps<{
initialSession: Session, // Supabase user session
}>) {
diff --git a/pages/generator/about.tsx b/pages/generator/about.tsx
new file mode 100644
index 00000000..daa696bb
--- /dev/null
+++ b/pages/generator/about.tsx
@@ -0,0 +1,19 @@
+import Row from 'react-bootstrap/Row';
+import Col from 'react-bootstrap/Col';
+import ListGroup from 'react-bootstrap/ListGroup';
+import SubPage from '../../generator/components/SubPage';
+
+export default function AboutPage() {
+ return (
+
+
+
+
+
+ Designed by Signal Kinetics
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/pages/generator/index.tsx b/pages/generator/index.tsx
new file mode 100644
index 00000000..29505658
--- /dev/null
+++ b/pages/generator/index.tsx
@@ -0,0 +1,38 @@
+import Row from 'react-bootstrap/Row';
+import Col from 'react-bootstrap/Col';
+import ListGroup from 'react-bootstrap/ListGroup';
+import NextLink from 'next/link';
+import Layout from '../../generator/components/Layout';
+
+export default function GeneratorIndexPage() {
+ return (
+
+
+
+
+
+
+ );
+
+ function ListGroupMenuItem ({ label, href }: {label: string, href: string}) {
+ return
+ {label}
+
+ }
+}
\ No newline at end of file
diff --git a/pages/generator/planet-editor.tsx b/pages/generator/planet-editor.tsx
new file mode 100644
index 00000000..adbec7c5
--- /dev/null
+++ b/pages/generator/planet-editor.tsx
@@ -0,0 +1,27 @@
+import Link from 'next/link';
+import Row from 'react-bootstrap/Row';
+import Col from 'react-bootstrap/Col';
+
+import SubPage from '../../generator/components/SubPage';
+import Controls from '../../generator/components/Controls';
+import { usePlanetEditorState } from '../../generator/hooks/use-planet-editor-state';
+import SceneDisplay from '../../generator/components/SceneDisplay';
+import PlanetEditorSceneManager from '../../generator/services/planet-editor-scene';
+
+const sceneManager = new PlanetEditorSceneManager(); // Then add a section to mint the image as an NFT
+
+export default function PlanetEditor () {
+ const planetState = usePlanetEditorState(sceneManager.planet);
+
+ return (
+
+
+