diff --git a/CLAUDE.md b/CLAUDE.md index f84afd6..0b3eb16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,3 +108,4 @@ Databases can be optionally encrypted. The `Encryption` class handles AES encryp - `VITE_SYNCDB_ENDPOINT`: Remote database sync endpoint Check `.env.local` for local configuration. +- document the toolId and material naming conventions. \ No newline at end of file diff --git a/docs/NAMING_CONVENTIONS.md b/docs/NAMING_CONVENTIONS.md new file mode 100644 index 0000000..9b2607b --- /dev/null +++ b/docs/NAMING_CONVENTIONS.md @@ -0,0 +1,138 @@ +# Naming Conventions + +## Tool and Material Naming + +This document describes the naming conventions used for tools, materials, and related entities in the immersive WebXR application. + +## Material Naming + +Materials follow a consistent naming pattern based on their color: + +**Format:** `material-{color}` + +**Where:** +- `{color}` is the hex string representation of the material's color (e.g., `#ff0000` for red) + +**Examples:** +- `material-#ff0000` - Red material +- `material-#00ff00` - Green material +- `material-#222222` - Dark gray material + +**Implementation:** +```typescript +const material = new StandardMaterial("material-" + color.toHexString(), scene); +``` + +**Location:** Materials are created in: +- `src/toolbox/functions/buildColor.ts` - For toolbox color swatches +- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Fallback material creation via `buildMissingMaterial()` + +## Tool Mesh Naming + +Tool meshes use a compound naming pattern that includes both the tool type and color: + +**Format:** `tool-{toolId}` + +**Where:** +- `{toolId}` = `{toolType}-{color}` +- `{toolType}` is a value from the `ToolType` enum (e.g., `BOX`, `SPHERE`, `CYLINDER`, `CONE`, `PLANE`, `PERSON`) +- `{color}` is the hex string representation of the tool's color + +**Examples:** +- `tool-BOX-#ff0000` - Red box tool +- `tool-SPHERE-#00ff00` - Green sphere tool +- `tool-CYLINDER-#0000ff` - Blue cylinder tool +- `tool-PLANE-#ffff00` - Yellow plane tool + +**Implementation:** +```typescript +function toolId(tool: ToolType, color: Color3) { + return tool + "-" + color.toHexString(); +} + +const newItem = await buildMesh(tool, `tool-${id}`, colorParent.getScene()); +// For example: `tool-BOX-#ff0000` +``` + +**Location:** Tool meshes are created in `src/toolbox/functions/buildTool.ts` + +## Tool Colors + +The application uses 16 predefined colors for the toolbox: + +```typescript +const colors: string[] = [ + "#222222", "#8b4513", "#006400", "#778899", // Row 1: Dark gray, Brown, Dark green, Light slate gray + "#4b0082", "#ff0000", "#ffa500", "#ffff00", // Row 2: Indigo, Red, Orange, Yellow + "#00ff00", "#00ffff", "#0000ff", "#ff00ff", // Row 3: Green, Cyan, Blue, Magenta + "#1e90ff", "#98fb98", "#ffe4b5", "#ff69b4" // Row 4: Dodger blue, Pale green, Moccasin, Hot pink +] +``` + +## Tool Types + +Available tool types from the `ToolType` enum: +- `BOX` - Cube mesh +- `SPHERE` - Sphere mesh +- `CYLINDER` - Cylinder mesh +- `CONE` - Cone mesh +- `PLANE` - Flat plane mesh +- `PERSON` - Person/avatar mesh + +## Material Color Access + +When accessing material colors, use this priority order to handle both current and legacy materials: + +```typescript +// For StandardMaterial +const stdMat = material as StandardMaterial; +const materialColor = stdMat.emissiveColor || stdMat.diffuseColor; + +// Current rendering uses emissiveColor +// Legacy materials may have diffuseColor instead +``` + +## Rendering Modes + +Materials can be rendered in three different modes, affecting how color properties are used: + +### 1. Lightmap with Lighting +- Uses `diffuseColor` + `lightmapTexture` +- `disableLighting = false` +- Most expensive performance-wise +- Provides lighting illusion with actual lighting calculations + +### 2. Unlit with Emissive Texture (Default) +- Uses `emissiveColor` + `emissiveTexture` (lightmap) +- `disableLighting = true` +- Best balance of visual quality and performance +- Provides lighting illusion without lighting calculations + +### 3. Flat Emissive +- Uses only `emissiveColor` +- `disableLighting = true` +- Best performance +- No lighting illusion, flat shading + +## Instance Naming + +Instanced meshes (created from tool templates) follow this pattern: + +**Format:** `tool-instance-{toolId}` + +**Example:** +```typescript +const instance = new InstancedMesh("tool-instance-" + id, newItem); +// For example: `tool-instance-BOX-#ff0000` +``` + +These instances share materials with their source mesh and are used for visual feedback before creating actual diagram entities. + +## Related Files + +- `src/toolbox/functions/buildTool.ts` - Tool mesh creation and naming +- `src/toolbox/functions/buildColor.ts` - Material creation and color management +- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Diagram entity instantiation +- `src/toolbox/types/toolType.ts` - ToolType enum definition +- `src/util/lightmapGenerator.ts` - Lightmap texture generation and caching +- `src/util/renderingMode.ts` - Rendering mode enum and labels diff --git a/server/etc/cert.pem b/server/etc/cert.pem new file mode 100644 index 0000000..f07bae0 --- /dev/null +++ b/server/etc/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJF3WqWLMk6JOlMA0GCSqGSIb3DQEBCwUAMCwxKjAoBgNV +BAMTIWRldi1nMGx0MThuZGJjcDZlYXJyLnVzLmF1dGgwLmNvbTAeFw0yNDA4MTUx +NTE1NDdaFw0zODA0MjQxNTE1NDdaMCwxKjAoBgNVBAMTIWRldi1nMGx0MThuZGJj +cDZlYXJyLnVzLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKfmuyqb2N8Tf5W9KTAy2D6FTkYICX+mcclyZS+0Mi7fLzSB7IeSuWXmuHoR +h5FHJ/Qp6eC1ahYs9WmAjFp81HPzZ/9hEbK3XrLMSta7zVldPTQjnt5sU/Zxr/M2 +xMjHH2P3G231si+G20czvDWoItnyWs8rcE2wEcyiXM+/Ixgxoh8kfc9pqpNLXTvM +IvqAuxXbPeju3XccQ6B0lshN72EwV9yW73B0s7DuHsbBA0WHKYmcvdXgnQ1dU2/L +8BR5s/gJJE0MUh2qhsnKE3yUC/hTW7A0Qn0SMEZey04hvJWePnn59kv52DPVXZpZ +ql6ISehwn3hZdhHjpsoHbE48CN0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUbjF1ri0QhHovlQ2D5gPtDucLGhowDgYDVR0PAQH/BAQDAgKEMA0G +CSqGSIb3DQEBCwUAA4IBAQAxIRNANDbVrkXF6x/KHYgj5prb+4yHtmYb7tiRi51z +MNHNLkltNou3dWsS4tU/YgzHTof3SJe2CIg9xAgk0XTHZjxRtbwIY6Zc9Sgf/KKL +OxFIiNcIQIGDoKHWmv2w4qSrYBkH9hva4kCysjgIFNc+0il7DQR2ifwLOxQGl/AE +hSfexgUKjfrno12gBlNCNcP+Xyn9/G++eg9vV+RuGLLIyLX0d0Vl7/C1pGoDrNpO +m/3oxR4IRnhEfGBD+LdWvmmIuxzXM1hSbLYJbMotHqKZSh0XlEM6Mi12gMZi7sEC +lhbXs+4ecvTBFfGCWFyUISFoSwRRnpQnEM5DsZT/t/Z8 +-----END CERTIFICATE----- diff --git a/server/etc/local.ini b/server/etc/local.ini new file mode 100644 index 0000000..ccf399d --- /dev/null +++ b/server/etc/local.ini @@ -0,0 +1,126 @@ +; CouchDB Configuration Settings + +; Custom settings should be made in this file. They will override settings +; in default.ini, but unlike changes made to default.ini, this file won't be +; overwritten on server upgrade. + +[couchdb] +database_dir = /var/snap/couchdb/common/data +view_index_dir = /var/snap/couchdb/common/data +;max_document_size = 4294967296 ; bytes +;os_process_timeout = 5000 +uuid = dd27b78cfb458b894e0277173f176878 + +[couch_peruser] +; If enabled, couch_peruser ensures that a private per-user database +; exists for each document in _users. These databases are writable only +; by the corresponding user. Databases are in the following form: +; userdb-{hex encoded username} +enable = true + +; If set to true and a user is deleted, the respective database gets +; deleted as well. +delete_dbs = true + +; Set a default q value for peruser-created databases that is different from +; cluster / q +;q = 1 +[log] +level = debug + +[chttpd] +;port = 5984 +bind_address = 127.0.0.1 +authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler} +;authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler} +; Options for the MochiWeb HTTP server. +;server_options = [{backlog, 128}, {acceptor_pool_size, 16}] + +; For more socket options, consult Erlang's module 'inet' man page. +;socket_options = [{sndbuf, 262144}, {nodelay, true}] +enable_cors = true + +[httpd] +; NOTE that this only configures the "backend" node-local port, not the +; "frontend" clustered port. You probably don't want to change anything in +; this section. +; Uncomment next line to trigger basic-auth popup on unauthorized requests. +;WWW-Authenticate = Basic realm="administrator" + +; Uncomment next line to set the configuration modification whitelist. Only +; whitelisted values may be changed via the /_config URLs. To allow the admin +; to change this value over HTTP, remember to include {httpd,config_whitelist} +; itself. Excluding it from the list would require editing this file to update +; the whitelist. +;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}] + +[ssl] +;enable = true +;cert_file = /full/path/to/server_cert.pem +;key_file = /full/path/to/server_key.pem +;password = somepassword + +; set to true to validate peer certificates +;verify_ssl_certificates = false + +; Set to true to fail if the client does not send a certificate. Only used if verify_ssl_certificates is true. +;fail_if_no_peer_cert = false + +; Path to file containing PEM encoded CA certificates (trusted +; certificates used for verifying a peer certificate). May be omitted if +; you do not want to verify the peer. +;cacert_file = /full/path/to/cacertf + +; The verification fun (optional) if not specified, the default +; verification fun will be used. +;verify_fun = {Module, VerifyFun} + +; maximum peer certificate depth +;ssl_certificate_max_depth = 1 + +; Reject renegotiations that do not live up to RFC 5746. +;secure_renegotiate = true + +; The cipher suites that should be supported. +; Can be specified in erlang format "{ecdhe_ecdsa,aes_128_cbc,sha256}" +; or in OpenSSL format "ECDHE-ECDSA-AES128-SHA256". +;ciphers = ["ECDHE-ECDSA-AES128-SHA256", "ECDHE-ECDSA-AES128-SHA"] + +; The SSL/TLS versions to support +;tls_versions = [tlsv1, 'tlsv1.1', 'tlsv1.2'] + +; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to +; the Virtual Host will be redirected to the path. In the example below all requests +; to http://example.com/ are redirected to /database. +; If you run CouchDB on a specific port, include the port number in the vhost: +; example.com:5984 = /database +[vhosts] +;example.com = /database/ + +; To create an admin account uncomment the '[admins]' section below and add a +; line in the format 'username = password'. When you next start CouchDB, it +; will change the password to a hash (so that your passwords don't linger +; around in plain-text files). You can add more admin accounts with more +; 'username = password' lines. Don't forget to restart CouchDB after +; changing this. +[admins] +admin = -pbkdf2-eeee185ee6142700c0e5a9e31b1d6d85ba952a49,f073f989f3201b55d953825d56acad2a,10 + +[chttpd_auth] +secret = a6bc1f1fd52803b4feae8f30b3944300 +;authentication_handlers = {chttpd_auth, jwt_authentication_handler} +[jwt_auth] +roles_claim_path = metadata.databases +required_claims = exp,iat +;validate_claim_iss = https://dev-g0lt18ndbcp6earr.us.auth0.com/ +;validate_claim_aud = sxAJub9Uo2mOE7iYCTOuQGhppGLEPWzb +[jwt_keys] +rsa:_default = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n + +rsa:1R0ZY6dzJ7ttWk60bT0_V = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n + +[cors] +origins = https://www.cybersecshield.com,https://cybersecshield.com,http://localhost:5173 +headers = accept, authorization, content-type, origin, referer +credentials = true +methods = GET, PUT, POST, HEAD, DELETE diff --git a/server/etc/pubkey.pem b/server/etc/pubkey.pem new file mode 100644 index 0000000..f55f7a4 --- /dev/null +++ b/server/etc/pubkey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY +PoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU +c/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi +2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v +YTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL ++FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI +3QIDAQAB +-----END PUBLIC KEY----- diff --git a/server/etc/vm.args b/server/etc/vm.args new file mode 100644 index 0000000..29c537d --- /dev/null +++ b/server/etc/vm.args @@ -0,0 +1,101 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# Each node in the system must have a unique name. These are specified through +# the Erlang -name flag, which takes the form: +# +# -name nodename@ +# +# or +# +# -name nodename@ +# +# CouchDB recommends the following values for this flag: +# +# 1. If this is a single node, not in a cluster, use: +# -name couchdb@127.0.0.1 +# +# 2. If DNS is configured for this host, use the FQDN, such as: +# -name couchdb@my.host.domain.com +# +# 3. If DNS isn't configured for this host, use IP addresses only, such as: +# -name couchdb@192.168.0.1 +# +# Do not rely on tricks with /etc/hosts or libresolv to handle anything +# other than the above 3 approaches correctly. They will not work reliably. +# +# Multiple CouchDBs running on the same machine can use couchdb1@, couchdb2@, +# etc. +-name couchdb@127.0.0.1 + +# All nodes must share the same magic cookie for distributed Erlang to work. +# Uncomment the following line and append a securely generated random value. +-setcookie eh.RauybPRHzP4-pXv + +# Which interfaces should the node listen on? +-kernel inet_dist_use_interface {127,0,0,1} + +# Tell kernel and SASL not to log anything +-kernel error_logger silent +-sasl sasl_error_logger false + +# This will toggle to true in Erlang 25+. However since we don't use global +# any longer, and have our own auto-connection module, we can keep the +# existing global behavior to avoid surprises. See +# https://github.com/erlang/otp/issues/6470#issuecomment-1337421210 for more +# information about possible increased coordination and messages being sent on +# disconnections when this setting is enabled. +# +-kernel prevent_overlapping_partitions false + +# Increase the pool of dirty IO schedulers from 10 to 16 +# Dirty IO schedulers are used for file IO. ++SDio 16 + +# Comment this line out to enable the interactive Erlang shell on startup ++Bd -noinput + +# Set maximum SSL session lifetime to reap terminated replication readers +-ssl session_lifetime 300 + +## TLS Distribution +## Use TLS for connections between Erlang cluster members. +## http://erlang.org/doc/apps/ssl/ssl_distribution.html +## +## Generate Cert(PEM) File +## This is just an example command to generate a certfile (PEM). +## This is not an endorsement of specific expiration limits, key sizes, or algorithms. +## $ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem +## $ cat key.pem cert.pem > dev/erlserver.pem && rm key.pem cert.pem +## +## Generate a Config File (couch_ssl_dist.conf) +## [{server, +## [{certfile, ""}, +## {secure_renegotiate, true}]}, +## {client, +## [{secure_renegotiate, true}]}]. +## +## CouchDB recommends the following values for no_tls flag: +## 1. Use TCP only, set to true, such as: +## -couch_dist no_tls true +## 2. Use TLS only, set to false, such as: +## -couch_dist no_tls false +## 3. Specify which node to use TCP, such as: +## -couch_dist no_tls \"*@127.0.0.1\" +## +## To ensure search works, make sure to set 'no_tls' option for the clouseau node. +## By default that would be "clouseau@127.0.0.1". +## Don't forget to override the paths to point to your certificate(s) and key(s)! +## +#-proto_dist couch +#-couch_dist no_tls '"clouseau@127.0.0.1"' +#-ssl_dist_optfile diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 2fa9490..27c39d5 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -5,6 +5,7 @@ import {Handle} from "../objects/handle"; import {DefaultScene} from "../defaultScene"; import {Button} from "../objects/Button"; import {LightmapGenerator} from "../util/lightmapGenerator"; +import {RenderingMode, RenderingModeLabels} from "../util/renderingMode"; const colors: string[] = [ "#222222", "#8b4513", "#006400", "#778899", @@ -21,6 +22,7 @@ export class Toolbox { private readonly _handle: Handle; private readonly _scene: Scene; private _xr?: WebXRDefaultExperience; + private _renderModeDisplay?: Button; constructor(readyObservable: Observable) { this._scene = DefaultScene.Scene; @@ -149,24 +151,126 @@ export class Toolbox { this._xr.baseExperience.onStateChangedObservable.add((state) => { if (state == 2) { // WebXRState.IN_XR - const button = Button.CreateButton("exitXr", "exitXr", this._scene, {}); + // Create exit XR button + const exitButton = Button.CreateButton("exitXr", "exitXr", this._scene, {}); // Position button at bottom-right of toolbox, matching handle size and orientation - button.transform.position.x = 0.5; // Right side - button.transform.position.y = -0.35; // Below color grid - button.transform.position.z = 0; // Coplanar with toolbox - button.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly - button.transform.scaling = new Vector3(.2, .2, .2); // Match handle height - button.transform.parent = this._toolboxBaseNode; + exitButton.transform.position.x = 0.5; // Right side + exitButton.transform.position.y = -0.35; // Below color grid + exitButton.transform.position.z = 0; // Coplanar with toolbox + exitButton.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly + exitButton.transform.scaling = new Vector3(.2, .2, .2); // Match handle height + exitButton.transform.parent = this._toolboxBaseNode; - button.onPointerObservable.add((evt) => { + exitButton.onPointerObservable.add((evt) => { this._logger.debug(evt); if (evt.sourceEvent.type == 'pointerdown') { this._xr.baseExperience.exitXRAsync(); } }); + + // Create rendering mode button that cycles through modes + this.createRenderModeButton(); } }); } + + private createRenderModeButton() { + const modes = [ + RenderingMode.LIGHTMAP_WITH_LIGHTING, + RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE, + RenderingMode.FLAT_EMISSIVE, + RenderingMode.DIFFUSE_WITH_LIGHTS + ]; + + const currentMode = LightmapGenerator.getRenderingMode(); + + this._renderModeDisplay = Button.CreateButton( + `Mode: ${RenderingModeLabels[currentMode]}`, + `renderModeButton`, + this._scene, + { + width: 0.5, + height: 0.2, + background: Color3.FromHexString("#333333"), + color: Color3.White(), + fontSize: 240 + } + ); + + // Position below the color grid + this._renderModeDisplay.transform.position.x = 0; + this._renderModeDisplay.transform.position.y = -.2; + this._renderModeDisplay.transform.position.z = 0; + this._renderModeDisplay.transform.rotation.y = Math.PI; + this._renderModeDisplay.transform.scaling = new Vector3(.4, .4, .4); + this._renderModeDisplay.transform.parent = this._toolboxBaseNode; + + // Add click handler to cycle through modes + this._renderModeDisplay.onPointerObservable.add((evt) => { + if (evt.sourceEvent.type == 'pointerdown') { + const currentMode = LightmapGenerator.getRenderingMode(); + const currentIndex = modes.indexOf(currentMode); + const nextIndex = (currentIndex + 1) % modes.length; + const nextMode = modes[nextIndex]; + + this._logger.info(`Cycling to rendering mode: ${nextMode}`); + LightmapGenerator.updateAllMaterials(this._scene, nextMode); + + // Update button text + this.updateRenderModeButton(nextMode); + } + }); + } + + private updateRenderModeButton(mode: RenderingMode) { + if (this._renderModeDisplay) { + // Dispose old button and create new one with updated text + this._renderModeDisplay.dispose(); + + this._renderModeDisplay = Button.CreateButton( + `Mode: ${RenderingModeLabels[mode]}`, + `renderModeButton`, + this._scene, + { + width: 0.5, + height: 0.2, + background: Color3.FromHexString("#333333"), + color: Color3.White(), + fontSize: 240 + } + ); + + this._renderModeDisplay.transform.position.x = 0; + this._renderModeDisplay.transform.position.y = -.2; + this._renderModeDisplay.transform.position.z = 0; + this._renderModeDisplay.transform.rotation.y = Math.PI; + this._renderModeDisplay.transform.scaling = new Vector3(.15, .15, .15); + this._renderModeDisplay.transform.parent = this._toolboxBaseNode; + + // Re-attach the click handler + this._renderModeDisplay.onPointerObservable.add((evt) => { + if (evt.sourceEvent.type == 'pointerdown') { + const modes = [ + RenderingMode.LIGHTMAP_WITH_LIGHTING, + RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE, + RenderingMode.FLAT_EMISSIVE, + RenderingMode.DIFFUSE_WITH_LIGHTS + ]; + + const currentMode = LightmapGenerator.getRenderingMode(); + const currentIndex = modes.indexOf(currentMode); + const nextIndex = (currentIndex + 1) % modes.length; + const nextMode = modes[nextIndex]; + + this._logger.info(`Cycling to rendering mode: ${nextMode}`); + LightmapGenerator.updateAllMaterials(this._scene, nextMode); + + // Update button text + this.updateRenderModeButton(nextMode); + } + }); + } + } } diff --git a/src/util/functions/sceneInspector.ts b/src/util/functions/sceneInspector.ts index d6aebf8..944e2d7 100644 --- a/src/util/functions/sceneInspector.ts +++ b/src/util/functions/sceneInspector.ts @@ -2,17 +2,13 @@ import {DefaultScene} from "../../defaultScene"; export function addSceneInspector() { window.addEventListener("keydown", (ev) => { - if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { - const web = document.querySelector('#webApp'); - (web as HTMLDivElement).style.display = 'none'; - + // Ctrl+Shift+I to open inspector + if (ev.shiftKey && ev.ctrlKey && !ev.altKey && ev.keyCode === 73) { import ("@babylonjs/inspector").then((inspector) => { inspector.Inspector.Show(DefaultScene.Scene, { overlay: true, showExplorer: true }); - const web = document.querySelector('#webApp'); - (web as HTMLDivElement).style.display = 'none'; }); /*import("@babylonjs/core/Debug").then(() => { import("@babylonjs/inspector").then(() => { diff --git a/src/util/lightmapGenerator.ts b/src/util/lightmapGenerator.ts index e7d0757..2af1128 100644 --- a/src/util/lightmapGenerator.ts +++ b/src/util/lightmapGenerator.ts @@ -1,5 +1,6 @@ -import {Color3, DynamicTexture, Scene} from "@babylonjs/core"; +import {Color3, DynamicTexture, HemisphericLight, PointLight, Scene, StandardMaterial, Vector3} from "@babylonjs/core"; import {DefaultScene} from "../defaultScene"; +import {RenderingMode} from "./renderingMode"; export class LightmapGenerator { private static lightmapCache: Map = new Map(); @@ -8,6 +9,13 @@ export class LightmapGenerator { // Toggle to enable/disable lightmap usage (for performance testing) public static ENABLED = true; + // Current rendering mode + private static currentMode: RenderingMode = RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE; + + // Scene lights for DIFFUSE_WITH_LIGHTS mode + private static hemisphericLight?: HemisphericLight; + private static pointLight?: PointLight; + /** * Generates or retrieves cached lightmap for a given color * @param color The base color for the lightmap @@ -125,4 +133,145 @@ export class LightmapGenerator { public static getCacheSize(): number { return this.lightmapCache.size; } + + /** + * Sets the rendering mode + * @param mode The rendering mode to use + */ + public static setRenderingMode(mode: RenderingMode): void { + this.currentMode = mode; + } + + /** + * Gets the current rendering mode + * @returns Current rendering mode + */ + public static getRenderingMode(): RenderingMode { + return this.currentMode; + } + + /** + * Applies the specified rendering mode to a material + * @param material The material to update + * @param color The base color + * @param mode The rendering mode to apply + * @param scene The BabylonJS scene + */ + public static applyRenderingModeToMaterial( + material: StandardMaterial, + color: Color3, + mode: RenderingMode, + scene: Scene + ): void { + // Clear existing textures and properties + material.diffuseColor = new Color3(0, 0, 0); + material.emissiveColor = new Color3(0, 0, 0); + material.diffuseTexture = null; + material.emissiveTexture = null; + material.lightmapTexture = null; + + switch (mode) { + case RenderingMode.LIGHTMAP_WITH_LIGHTING: + // Use diffuseColor + lightmapTexture with lighting enabled + material.diffuseColor = color; + material.lightmapTexture = this.generateLightmapForColor(color, scene); + material.useLightmapAsShadowmap = false; + material.disableLighting = false; + break; + + case RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE: + // Use emissiveColor + emissiveTexture with lighting disabled + material.emissiveColor = color; + material.emissiveTexture = this.generateLightmapForColor(color, scene); + material.disableLighting = true; + break; + + case RenderingMode.FLAT_EMISSIVE: + // Use only emissiveColor with lighting disabled + material.emissiveColor = color; + material.disableLighting = true; + break; + + case RenderingMode.DIFFUSE_WITH_LIGHTS: + // Use diffuseColor with dynamic lighting enabled + material.diffuseColor = color; + material.disableLighting = false; + break; + } + } + + /** + * Creates or enables scene lights for DIFFUSE_WITH_LIGHTS mode + * @param scene The BabylonJS scene + */ + private static createSceneLights(scene: Scene): void { + if (!this.hemisphericLight) { + this.hemisphericLight = new HemisphericLight("renderModeHemiLight", new Vector3(0, 1, 0), scene); + this.hemisphericLight.intensity = 0.7; + } + + if (!this.pointLight) { + this.pointLight = new PointLight("renderModePointLight", new Vector3(2, 3, 2), scene); + this.pointLight.intensity = 0.8; + } + + this.hemisphericLight.setEnabled(true); + this.pointLight.setEnabled(true); + } + + /** + * Disables scene lights used for DIFFUSE_WITH_LIGHTS mode + */ + private static disableSceneLights(): void { + if (this.hemisphericLight) { + this.hemisphericLight.setEnabled(false); + } + if (this.pointLight) { + this.pointLight.setEnabled(false); + } + } + + /** + * Updates all materials in the scene to use the specified rendering mode + * @param scene The BabylonJS scene + * @param mode The rendering mode to apply + */ + public static updateAllMaterials(scene: Scene, mode: RenderingMode): void { + this.currentMode = mode; + + // Enable or disable scene lights based on mode + if (mode === RenderingMode.DIFFUSE_WITH_LIGHTS) { + this.createSceneLights(scene); + } else { + this.disableSceneLights(); + } + + scene.materials.forEach(material => { + if (material instanceof StandardMaterial) { + // Skip UI materials (buttons, handles, and labels use emissiveTexture with text rendering) + if (material.name === 'buttonMat' || + material.name === 'handleMaterial' || + material.name === 'text-mat' || + material.id.includes('button') || + material.id.includes('handle') || + material.id.includes('text')) { + return; + } + + // Try to determine the base color from existing material + let baseColor: Color3; + + if (material.emissiveColor && material.emissiveColor.toLuminance() > 0) { + baseColor = material.emissiveColor.clone(); + } else if (material.diffuseColor && material.diffuseColor.toLuminance() > 0) { + baseColor = material.diffuseColor.clone(); + } else { + // Skip materials without a color set + return; + } + + this.applyRenderingModeToMaterial(material, baseColor, mode, scene); + } + }); + } } diff --git a/src/util/renderingMode.ts b/src/util/renderingMode.ts new file mode 100644 index 0000000..5671355 --- /dev/null +++ b/src/util/renderingMode.ts @@ -0,0 +1,36 @@ +/** + * Rendering modes for materials in the scene + * + * LIGHTMAP_WITH_LIGHTING: Uses diffuseColor + lightmapTexture with lighting enabled + * - Provides lighting illusion with actual lighting calculations + * - Most expensive performance-wise + * - disableLighting = false + * + * UNLIT_WITH_EMISSIVE_TEXTURE: Uses emissiveColor + emissiveTexture with lighting disabled + * - Provides lighting illusion without lighting calculations (current default) + * - Best balance of visual quality and performance + * - disableLighting = true + * + * FLAT_EMISSIVE: Uses only emissiveColor with lighting disabled + * - Flat shading, no lighting illusion + * - Best performance + * - disableLighting = true + * + * DIFFUSE_WITH_LIGHTS: Uses diffuseColor with two scene lights enabled + * - Real-time lighting calculations with dynamic lights + * - Provides realistic lighting and shadows + * - disableLighting = false + */ +export enum RenderingMode { + LIGHTMAP_WITH_LIGHTING = "lightmap_with_lighting", + UNLIT_WITH_EMISSIVE_TEXTURE = "unlit_with_emissive_texture", + FLAT_EMISSIVE = "flat_emissive", + DIFFUSE_WITH_LIGHTS = "diffuse_with_lights" +} + +export const RenderingModeLabels = { + [RenderingMode.LIGHTMAP_WITH_LIGHTING]: "Lightmap + Lighting", + [RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE]: "Emissive Texture", + [RenderingMode.FLAT_EMISSIVE]: "Flat Color", + [RenderingMode.DIFFUSE_WITH_LIGHTS]: "Diffuse + Lights" +};