diff --git a/src/DEV-NOTES.md b/src/DEV-NOTES.md index da88f63b..d9299b06 100644 --- a/src/DEV-NOTES.md +++ b/src/DEV-NOTES.md @@ -2,6 +2,11 @@ These is a place to save random notes like code snippets, links to assets, and other references. This doc might not be useful to anyone else :) +### Example managed street load from hash +``` +localhost:3333#managed-street-json:{"id":"aaaaaaaa-0123-4678-9000-000000000000","name":"Kieran's Awesome Street","width":40,"length":100,"justifyWidth":"center","justifyLength":"start","segments":[{"id":"aaaaaaaa-0123-4678-9000-000000000001","name":"Sidewalk for walking","type":"sidewalk","surface":"sidewalk","color":"#ffffff","level":1,"width":3,"direction":"none","generated":{"pedestrians":[{"density":"normal"}]}},{"id":"aaaaaaaa-0123-4678-9000-000000000002","name":"Sidewalk for trees and stuff","type":"sidewalk","surface":"sidewalk","color":"#ffffff","level":1,"width":1,"direction":"none","generated":{"clones":[{"mode":"fixed","model":"tree3","spacing":15}]}},{"id":"aaaaaaaa-0123-4678-9000-000000000003","name":"Parking for cars","type":"parking-lane","surface":"concrete","color":"#dddddd","level":0,"width":3,"direction":"inbound","generated":{"clones":[{"mode":"random","modelsArray":"sedan-rig, self-driving-waymo-car, suv-rig","spacing":6,"count":6}],"stencil":[{"model":"parking-t","cycleOffset":1,"spacing":6}]}},{"id":"aaaaaaaa-0123-4678-9000-000000000004","name":"Drive Lane for cars and stuff","type":"drive-lane","color":"#ffffff","surface":"asphalt","level":0,"width":3,"direction":"inbound","generated":{"clones":[{"mode":"random","modelsArray":"sedan-rig, box-truck-rig, self-driving-waymo-car, suv-rig, motorbike","spacing":7.3,"count":4}]}},{"id":"aaaaaaaa-0123-4678-9000-000000000005","name":"A beautiful median","type":"divider","surface":"sidewalk","color":"#ffffff","level":1,"width":0.5}]} +``` + ### Audio Notes ``` var entity = document.querySelector('.playme'); diff --git a/src/README.md b/src/README.md index 28e88af0..76b5f749 100644 --- a/src/README.md +++ b/src/README.md @@ -106,5 +106,17 @@ I learned a few things: * This UUID is not shown in the UI. It can be found by going to this URL and supplying the nameSpacedId and creatorId, such as: https://streetmix.net/api/v1/streets?namespacedId=3&creatorId=kfarr . This will redirect to the UUID API endpoint * I wrote a quick JS helper function that takes a user facing URL on Streetmix (such as https://streetmix.net/kfarr/3/a-frame-city-builder-street-only) and transforms it into the API Redirect to find the UUID endpoint. You can find the [helper function docs here](https://github.com/kfarr/3dstreet/tree/master/src#streetmix-utilsjs). +# Possibly Accepted URL Input hash schemes + +3DStreet can import third-party street data in a variety of formats. The following URL input hash schemes are experimental and not guaranteed to be supported in future versions: + +| Scheme | Description | Usage Example | +| --------- | -- |-- | +| `streetmix-url` | Streetmix User-Facing Street URL | `https://3dstreet.app/#https://streetmix.net/kfarr/3/3dstreet-demo-street` | +| `streetplan-url` | StreetPlan API URL | `https://3dstreet.app/#https://streetplan.net/3dstreet/89241` | +| `managed-street-json` | Managed Street JSON Blob | `https://3dstreet.app/#managed-street-json:{"data":"value"}` | +| `cloud-uuid-legacy` | 3DStreet Scene JSON Format from Cloud UUID with .json Extension | `https://3dstreet.app/#scenes/bc72ab26-891d-417b-a50f-0cf84621a54c.json` | +| `cloud-uuid` | 3DStreet Scene JSON Format from Cloud UUID | `https://3dstreet.app/#scenes/bc72ab26-891d-417b-a50f-0cf84621a54c` | + ### More Notes See [DEV-NOTES](DEV-NOTES.md) for additional notes on future features and work in progress. diff --git a/src/components/managed-street.js b/src/components/managed-street.js index d70cdabc..dc75e9db 100644 --- a/src/components/managed-street.js +++ b/src/components/managed-street.js @@ -5,14 +5,6 @@ const { segmentVariants } = require('../segments-variants.js'); const streetmixUtils = require('../tested/streetmix-utils'); const streetmixParsersTested = require('../tested/aframe-streetmix-parsers-tested'); -// invoking from js console -/* -userLayersEl = document.getElementById('street-container'); -newStreetEl = document.createElement('a-entity'); -newStreetEl.setAttribute('managed-street', 'sourceType: streetmix-url; sourceValue: https://streetmix.net/kfarr/3/; synchronize: true'); -userLayersEl.append(newStreetEl); -*/ - AFRAME.registerComponent('managed-street', { schema: { width: { @@ -98,9 +90,18 @@ AFRAME.registerComponent('managed-street', { if (data.sourceType === 'streetmix-url') { this.loadAndParseStreetmixURL(data.sourceValue); } else if (data.sourceType === 'streetplan-url') { + // this function is not yet implemented this.refreshFromStreetplanURL(data.sourceValue); } else if (data.sourceType === 'json-blob') { - this.refreshFromJSONBlob(data.sourceValue); + // if data.sourceValue is a string convert string to object for parsing but keep string for saving + if (typeof data.sourceValue === 'string') { + const streetObjectFromBlob = JSON.parse(data.sourceValue); + this.parseStreetObject(streetObjectFromBlob); + } else { + console.log( + '[managed-street]: ERROR parsing json-blob, sourceValue must be a string' + ); + } } }, applyLength: function () { @@ -174,6 +175,8 @@ AFRAME.registerComponent('managed-street', { this.justifiedDirtBox = dirtBox; dirtBox.setAttribute('material', `color: ${window.STREET.colors.brown};`); dirtBox.setAttribute('data-layer-name', 'Underground'); + dirtBox.setAttribute('data-no-transform', ''); + dirtBox.setAttribute('data-ignore-raycaster', ''); } this.justifiedDirtBox.setAttribute('height', 2); // height is 2 meters from y of -0.1 to -y of 2.1 this.justifiedDirtBox.setAttribute('width', streetWidth); @@ -202,6 +205,42 @@ AFRAME.registerComponent('managed-street', { `${xPosition} -1 ${zPosition}` ); }, + parseStreetObject: function (streetObject) { + // reset and delete all existing entities + this.remove(); + + // given an object streetObject, create child entities with 'street-segment' component + this.el.setAttribute( + 'data-layer-name', + 'Managed Street • ' + streetObject.name + ); + this.el.setAttribute('managed-street', 'width', streetObject.width); + this.el.setAttribute('managed-street', 'length', streetObject.length); + + for (let i = 0; i < streetObject.segments.length; i++) { + const segment = streetObject.segments[i]; + const segmentEl = document.createElement('a-entity'); + this.el.appendChild(segmentEl); + + segmentEl.setAttribute('street-segment', { + type: segment.type, // this is the base type, it won't load its defaults since we are changing more than just the type value + width: segment.width, + length: streetObject.length, + level: segment.level, + direction: segment.direction, + color: segment.color || window.STREET.types[segment.type]?.color, + surface: segment.surface || window.STREET.types[segment.type]?.surface // no error handling for segmentPreset not found + }); + segmentEl.setAttribute('data-layer-name', segment.name); + // wait for street-segment to be loaded, then generate components from segment object + segmentEl.addEventListener('loaded', () => { + segmentEl.components[ + 'street-segment' + ].generateComponentsFromSegmentObject(segment); + this.applyJustification(); + }); + } + }, loadAndParseStreetmixURL: async function (streetmixURL) { const data = this.data; const streetmixAPIURL = streetmixUtils.streetmixUserToAPI(streetmixURL); @@ -265,6 +304,7 @@ AFRAME.registerComponent('managed-street', { // When all entities are loaded, do something with them this.allLoadedPromise.then(() => { + this.refreshManagedEntities(); this.applyJustification(); this.createOrUpdateJustifiedDirtBox(); AFRAME.INSPECTOR.selectEntity(this.el); @@ -293,6 +333,8 @@ AFRAME.registerComponent('managed-street', { } }); +// Helper functions for Streetmix to A-Frame conversion + function getSeparatorMixinId(previousSegment, currentSegment) { if (previousSegment === undefined || currentSegment === undefined) { return null; diff --git a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js index 7a92c83b..eed592dd 100644 --- a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js +++ b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js @@ -1,5 +1,6 @@ import { loadScript, roundCoord } from '../../../../../src/utils.js'; import { createUniqueId } from '../../../lib/entity.js'; +import * as defaultStreetObjects from './defaultStreets.js'; export function createSvgExtrudedEntity(position) { // This component accepts a svgString and creates a new entity with geometry extruded @@ -50,7 +51,7 @@ export function createMapbox() { }); } -export function createManagedStreet(position) { +export function createManagedStreetFromStreetmixURLPrompt(position) { // This creates a new Managed Street let streetmixURL = prompt( 'Please enter a Streetmix URL', @@ -76,7 +77,29 @@ export function createManagedStreet(position) { } } +export function createManagedStreetFromStreetObject(position, streetObject) { + // This creates a new Managed Street + if (streetObject && streetObject !== '') { + const definition = { + id: createUniqueId(), + components: { + position: position ?? '0 0 0', + 'managed-street': { + sourceType: 'json-blob', + sourceValue: JSON.stringify(streetObject), + showVehicles: true, + showStriping: true, + synchronize: true + } + } + }; + + AFRAME.INSPECTOR.execute('entitycreate', definition); + } +} + export function createStreetmixStreet(position, streetmixURL, hideBuildings) { + // legacy // This code snippet allows the creation of an additional Streetmix street // in your 3DStreet scene without replacing any existing streets. if (streetmixURL === undefined) { @@ -116,6 +139,18 @@ export function create60ftRightOfWay(position) { true ); } + +export function create60ftRightOfWayManagedStreet(position) { + console.log( + 'create60ftRightOfWayManagedStreet', + defaultStreetObjects.stroad60ftROW + ); + createManagedStreetFromStreetObject( + position, + defaultStreetObjects.stroad60ftROW + ); +} + export function create80ftRightOfWay(position) { createStreetmixStreet( position, diff --git a/src/editor/components/components/AddLayerPanel/defaultStreets.js b/src/editor/components/components/AddLayerPanel/defaultStreets.js new file mode 100644 index 00000000..098f9f30 --- /dev/null +++ b/src/editor/components/components/AddLayerPanel/defaultStreets.js @@ -0,0 +1,324 @@ +// Define some example streets in Managed Street object format + +export const stroad60ftROW = { + id: '2d729802-6d80-45fa-89bd-f6d6b120d936', + name: '60ft Right of Way 36ft Road Width', + width: 18.288, // Keep in meters + length: 100, + justifyWidth: 'center', + justifyLength: 'start', + segments: [ + { + id: 'JCWzsLQHmyfDHzQhi9_pU', + name: 'Dense Sidewalk', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 1.829, + direction: 'none', + generated: { + pedestrians: [ + { + density: 'dense' + } + ] + } + }, + { + id: 'RsLZFtSi3oJH7uufQ5rc4', + name: 'Tree Planting Strip', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 0.914, + direction: 'none', + generated: { + clones: [ + { + mode: 'fixed', + model: 'tree3', + spacing: 15 + } + ] + } + }, + { + id: 'Xf2CNmHkMaGkTM8EaJn6h', + name: 'Modern Street Lamp', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 0.914, + direction: 'none', + generated: { + clones: [ + { + mode: 'fixed', + model: 'lamp-modern', + spacing: 30, + facing: 0 + } + ] + } + }, + { + id: 'GbEHhCMPmVom_IJK-xIn3', + name: 'Inbound Parking', + type: 'parking-lane', + surface: 'concrete', + color: '#dddddd', + level: 0, + width: 2.438, + direction: 'inbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: 'sedan-rig, self-driving-waymo-car, suv-rig', + spacing: 6, + count: 6 + } + ], + stencil: [ + { + model: 'parking-t', + cycleOffset: 1, + spacing: 6 + } + ] + } + }, + { + id: 'z4gZgzYoM7sQ7mzIV01PC', + name: 'Inbound Drive Lane', + type: 'drive-lane', + surface: 'asphalt', + color: '#ffffff', + level: 0, + width: 3.048, + direction: 'inbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: + 'sedan-rig, box-truck-rig, self-driving-waymo-car, suv-rig, motorbike', + spacing: 7.3, + count: 4 + } + ] + } + }, + { + id: 'myp8_d3x_-hwuhTyH8ux1', + name: 'Outbound Drive Lane', + type: 'drive-lane', + surface: 'asphalt', + color: '#ffffff', + level: 0, + width: 3.048, + direction: 'outbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: + 'sedan-rig, box-truck-rig, self-driving-waymo-car, suv-rig, motorbike', + spacing: 7.3, + count: 4 + } + ] + } + }, + { + id: 'ARosTXeWGXp17QyfZgSKB', + name: 'Outbound Parking', + type: 'parking-lane', + surface: 'concrete', + color: '#dddddd', + level: 0, + width: 2.438, + direction: 'outbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: 'sedan-rig, self-driving-waymo-car, suv-rig', + spacing: 6, + count: 6 + } + ], + stencil: [ + { + model: 'parking-t', + cycleOffset: 1, + spacing: 6 + } + ] + } + }, + { + id: 'oweuZgwBHUbt65Ep7GZhU', + name: 'Modern Street Lamp', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 0.914, + direction: 'none', + generated: { + clones: [ + { + mode: 'fixed', + model: 'lamp-modern', + spacing: 30, + facing: 180 + } + ] + } + }, + { + id: 'vL9qDNp5neZt32zlZ9ExG', + name: 'Tree Planting Strip', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 0.914, + direction: 'none', + generated: { + clones: [ + { + mode: 'fixed', + model: 'tree3', + spacing: 15 + } + ] + } + }, + { + id: 'RClRRZoof9_BYnqQm7mz-', + name: 'Normal Sidewalk', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 1.829, + direction: 'none', + generated: { + pedestrians: [ + { + density: 'normal' + } + ] + } + } + ] +}; + +export const exampleStreet = { + id: 'aaaaaaaa-0123-4678-9000-000000000000', + name: "Kieran's Basic Street", + width: 40, + length: 100, + justifyWidth: 'center', + justifyLength: 'start', + segments: [ + { + id: 'aaaaaaaa-0123-4678-9000-000000000001', + name: 'Sidewalk for walking', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 3, + direction: 'none', + generated: { + pedestrians: [ + { + density: 'normal' + } + ] + } + }, + { + id: 'aaaaaaaa-0123-4678-9000-000000000002', + name: 'Sidewalk for trees and stuff', + type: 'sidewalk', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 1, + direction: 'none', + generated: { + clones: [ + { + mode: 'fixed', + model: 'tree3', + spacing: 15 + } + ] + } + }, + { + id: 'aaaaaaaa-0123-4678-9000-000000000003', + name: 'Parking for cars', + type: 'parking-lane', + surface: 'concrete', + color: '#dddddd', + level: 0, + width: 3, + direction: 'inbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: 'sedan-rig, self-driving-waymo-car, suv-rig', + spacing: 6, + count: 6 + } + ], + stencil: [ + { + model: 'parking-t', + cycleOffset: 1, + spacing: 6 + } + ] + } + }, + { + id: 'aaaaaaaa-0123-4678-9000-000000000004', + name: 'Drive Lane for cars and stuff', + type: 'drive-lane', + color: '#ffffff', + surface: 'asphalt', + level: 0, + width: 3, + direction: 'inbound', + generated: { + clones: [ + { + mode: 'random', + modelsArray: + 'sedan-rig, box-truck-rig, self-driving-waymo-car, suv-rig, motorbike', + spacing: 7.3, + count: 4 + } + ] + } + }, + { + id: 'aaaaaaaa-0123-4678-9000-000000000005', + name: 'A beautiful median', + type: 'divider', + surface: 'sidewalk', + color: '#ffffff', + level: 1, + width: 0.5 + } + ] +}; diff --git a/src/editor/components/components/AddLayerPanel/layersData.js b/src/editor/components/components/AddLayerPanel/layersData.js index 42278192..c20b2b37 100644 --- a/src/editor/components/components/AddLayerPanel/layersData.js +++ b/src/editor/components/components/AddLayerPanel/layersData.js @@ -1,17 +1,4 @@ -import { - createSvgExtrudedEntity, - createStreetmixStreet, - createCustomModel, - createPrimitiveGeometry, - createIntersection, - create40ftRightOfWay, - create60ftRightOfWay, - create80ftRightOfWay, - create94ftRightOfWay, - create150ftRightOfWay, - createImageEntity, - createManagedStreet -} from './createLayerFunctions'; +import * as createFunctions from './createLayerFunctions'; export const streetLayersData = [ { @@ -21,7 +8,7 @@ export const streetLayersData = [ description: 'Create an additional Streetmix street in your 3DStreet scene without replacing any existing streets.', id: 1, - handlerFunction: createStreetmixStreet + handlerFunction: createFunctions.createStreetmixStreet }, { name: '40ft RoW / 24ft Roadway Width', @@ -29,7 +16,7 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/streetmix24.png', description: 'Premade Street 40ft Right of Way / 24ft Roadway Width', id: 2, - handlerFunction: create40ftRightOfWay + handlerFunction: createFunctions.create40ftRightOfWay }, { name: '60ft RoW / 36ft Roadway Width', @@ -37,7 +24,7 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/streetmix24.png', description: 'Premade Street 60ft Right of Way / 36ft Roadway Width', id: 3, - handlerFunction: create60ftRightOfWay + handlerFunction: createFunctions.create60ftRightOfWay }, { name: '80ft RoW / 56ft Roadway Width', @@ -45,7 +32,7 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/streetmix24.png', description: 'Premade Street 80ft Right of Way / 56ft Roadway Width', id: 4, - handlerFunction: create80ftRightOfWay + handlerFunction: createFunctions.create80ftRightOfWay }, { name: '94ft RoW / 70ft Roadway Width', @@ -53,7 +40,7 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/streetmix24.png', description: 'Premade Street 94ft Right of Way / 70ft Roadway Width', id: 5, - handlerFunction: create94ftRightOfWay + handlerFunction: createFunctions.create94ftRightOfWay }, { name: '150ft RoW / 124ft Roadway Width', @@ -61,7 +48,7 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/streetmix24.png', description: 'Premade Street 150ft Right of Way / 124ft Roadway Width', id: 6, - handlerFunction: create150ftRightOfWay + handlerFunction: createFunctions.create150ftRightOfWay }, { name: 'Create intersection', @@ -71,17 +58,25 @@ export const streetLayersData = [ description: 'Create intersection entity. Parameters of intersection component could be changed in properties panel.', id: 7, - handlerFunction: createIntersection + handlerFunction: createFunctions.createIntersection }, { - name: 'Create Managed Street (Beta)', + name: '(Beta) Managed Street from Streetmix URL', img: '', requiresPro: true, icon: '', description: - 'Create a new street from Streetmix using the Managed Street component.', + 'Create a new street from Streetmix URL using the Managed Street component.', id: 8, - handlerFunction: createManagedStreet + handlerFunction: createFunctions.createManagedStreetFromStreetmixURLPrompt + }, + { + name: '(Beta) Managed Street 60ft RoW / 36ft Roadway Width', + img: 'ui_assets/cards/street-preset-60-36.jpg', + icon: 'ui_assets/cards/icons/3dst24.png', + description: 'Premade Street 60ft Right of Way / 36ft Roadway Width', + id: 9, + handlerFunction: createFunctions.create60ftRightOfWayManagedStreet } ]; @@ -94,7 +89,7 @@ export const customLayersData = [ description: 'Create entity with svg-extruder component, that accepts a svgString and creates a new entity with geometry extruded from the svg and applies the default mixin material grass.', id: 1, - handlerFunction: createSvgExtrudedEntity + handlerFunction: createFunctions.createSvgExtrudedEntity }, { name: 'glTF model from URL', @@ -104,7 +99,7 @@ export const customLayersData = [ description: 'Create entity with model from path for a glTF (or Glb) file hosted on any publicly accessible HTTP server.', id: 2, - handlerFunction: createCustomModel + handlerFunction: createFunctions.createCustomModel }, { name: 'Create primitive geometry', @@ -114,7 +109,7 @@ export const customLayersData = [ description: 'Create entity with A-Frame primitive geometry. Geometry type could be changed in properties panel.', id: 3, - handlerFunction: createPrimitiveGeometry + handlerFunction: createFunctions.createPrimitiveGeometry }, { name: 'Place New Image Entity', @@ -124,6 +119,6 @@ export const customLayersData = [ description: 'Place an image such as a sign, reference photo, custom map, etc.', id: 4, - handlerFunction: createImageEntity + handlerFunction: createFunctions.createImageEntity } ]; diff --git a/src/json-utils_1.1.js b/src/json-utils_1.1.js index ebabe72b..dac6e2cb 100644 --- a/src/json-utils_1.1.js +++ b/src/json-utils_1.1.js @@ -487,6 +487,35 @@ AFRAME.registerComponent('set-loader-from-hash', { if (!streetURL) { return; } + if (streetURL.startsWith('managed-street-json:')) { + // url.com/page#managed-street-json:{"data":"value"} + const fragment = window.location.hash; + const prefix = '#managed-street-json:'; + + let jsonStr = {}; + try { + const encodedJsonStr = fragment.substring(prefix.length); + jsonStr = decodeURIComponent(encodedJsonStr); + } catch (err) { + console.error('Error parsing fragment:', err); + } + const definition = { + components: { + 'managed-street': { + sourceType: 'json-blob', + sourceValue: jsonStr, + synchronize: true + } + } + }; + // use set timeout + setTimeout(() => { + AFRAME.INSPECTOR.execute('entitycreate', definition); + // street notify + STREET.notify.successMessage('Loading Managed Street JSON from URL'); + }, 1000); + return; + } if (streetURL.includes('//streetmix.net')) { console.log( '[set-loader-from-hash]', diff --git a/ui_assets/cards/icons/3dst24.png b/ui_assets/cards/icons/3dst24.png new file mode 100644 index 00000000..c56ce444 Binary files /dev/null and b/ui_assets/cards/icons/3dst24.png differ