diff --git a/src/assets.js b/src/assets.js
index 0ebdd4a50..4c9f6db3c 100644
--- a/src/assets.js
+++ b/src/assets.js
@@ -147,6 +147,7 @@ function buildAssetHTML(assetUrl, categories) {
+
diff --git a/src/components/managed-street.js b/src/components/managed-street.js
index 32bdcf0ff..285ce8d88 100644
--- a/src/components/managed-street.js
+++ b/src/components/managed-street.js
@@ -5,6 +5,206 @@ const { segmentVariants } = require('../segments-variants.js');
const streetmixUtils = require('../tested/streetmix-utils');
const streetmixParsersTested = require('../tested/aframe-streetmix-parsers-tested');
+// STREETPLAN HELPER FUNCTIONS
+// Material mapping from Streetplan to 3DStreet surfaces
+const STREETPLAN_MATERIAL_MAPPING = {
+ 'asphalt black': { surface: 'asphalt', color: '#aaaaaa' },
+ 'asphalt blue': { surface: 'asphalt', color: '#aaaaff' },
+ 'asphalt red 1': { surface: 'asphalt', color: '#ffaaaa' },
+ 'asphalt red 2': { surface: 'asphalt', color: '#ff0000' },
+ 'asphalt green': { surface: 'asphalt', color: '#aaffaa' },
+ 'asphalt old': { surface: 'asphalt' },
+ 'standard concrete': { surface: 'concrete' },
+ grass: { surface: 'grass' },
+ 'grass dead': { surface: 'grass' },
+ 'pavers tan': { surface: 'sidewalk' },
+ 'pavers brown': { surface: 'sidewalk' },
+ 'pavers mixed': { surface: 'sidewalk' },
+ 'pavers red': { surface: 'sidewalk', color: '#ffaaaa' },
+ 'tint conc. or dirt': { surface: 'gravel' },
+ dirt: { surface: 'gravel' },
+ gravel: { surface: 'gravel' },
+ stonetan: { surface: 'sidewalk' },
+ 'sidewalk 2': { surface: 'sidewalk' },
+ 'cobble stone': { surface: 'sidewalk' },
+ 'solid black': { surface: 'solid' },
+ 'painted intersection': { surface: 'asphalt' },
+ 'grass with edging': { surface: 'grass' },
+ xeriscape: { surface: 'grass' },
+ 'grassslopemedian 12ft': { surface: 'grass' },
+ 'grassslopemedian 24ft': { surface: 'grass' },
+ 'grassslope 12ft-left': { surface: 'grass' },
+ 'grassslope 12ft-right': { surface: 'grass' },
+ 'grassslope 24ft-left': { surface: 'grass' },
+ 'grassslope 24ft-right': { surface: 'grass' },
+ sand: { surface: 'sand' }
+};
+
+const STREETPLAN_OBJECT_MAPPING = {
+ 'away, left park, head in': '',
+ 'barrier 1-ft': 'temporary-jersey-barrier-concrete',
+ 'barrier 2-ft': 'temporary-jersey-barrier-concrete',
+ 'bike food cart': '',
+ 'bikelane sharecar': '',
+ 'bikerack bollard': '',
+ 'blank pedrefuge (8ft)': '',
+ 'blue car': 'sedan-rig',
+ 'blue mailbox': 'usps-mailbox',
+ 'bollard plastic yellow': 'bollard',
+ boulevardcirculator: 'minibus',
+ 'boulevardcirculator rev': 'minibus',
+ 'boxwood planter 2ft': 'dividers-planter-box',
+ 'boxwood planter 3ft': 'dividers-planter-box',
+ 'boxwood planter 5ft': 'dividers-planter-box',
+ 'bur oak': 'tree3',
+ bus: 'bus',
+ 'bus rev': 'bus',
+ 'cactus median (10ft)': 'dividers-bush',
+ 'cactus median (12ft)': 'dividers-bush',
+ 'cactus median (4ft)': 'dividers-bush',
+ 'cactus median (6ft)': 'dividers-bush',
+ 'cactus median (8ft)': 'dividers-bush',
+ 'casual woman': '',
+ couple: '',
+ 'couple biking': '',
+ 'desertwillow texas': 'tree3',
+ 'dog walker': '',
+ 'empty place holder': '',
+ 'english oak': 'tree3',
+ 'fleamarket stuff': '',
+ 'flower median (10ft)': 'dividers-flowers',
+ 'flower median (12ft)': 'dividers-flowers',
+ 'flower median (4ft)': 'dividers-flowers',
+ 'flower median (6ft)': 'dividers-flowers',
+ 'flower median (8ft)': 'dividers-flowers',
+ 'flower pot 4ft': 'dividers-flowers',
+ 'floweringpear 18ft': 'tree3',
+ 'flowers pedrefuge (8ft)': 'dividers-flowers',
+ goldenraintree: 'tree3',
+ 'golfcart red 4ft back': 'tuk-tuk',
+ 'grassmound (10ft)': '',
+ 'grassmound (12ft)': '',
+ 'grassmound (4ft)': '',
+ 'grassmound (6ft)': '',
+ 'grassmound (8ft)': '',
+ 'grassy median (10ft)': '',
+ 'grassy median (12ft)': '',
+ 'grassy median (4ft)': '',
+ 'grassy median (6ft)': '',
+ 'grassy median (8ft)': '',
+ 'green car': 'sedan-rig',
+ 'heavy rail': 'tram',
+ 'heavy rail rev': 'tram',
+ 'historic light': 'lamp-traditional',
+ 'historic no banner': 'lamp-traditional',
+ 'historic with banners': 'lamp-traditional',
+ 'historic with flowers 1': 'lamp-traditional',
+ 'historic with flowers 2': 'lamp-traditional',
+ honeylocust: 'tree3',
+ 'japanese lilac': 'tree3',
+ 'japanese zelkova': 'tree3',
+ 'jerusalem thorn': 'tree3',
+ 'kentucky coffeetree': 'tree3',
+ 'large food cart': '',
+ 'large oak': 'tree3',
+ 'light rail poles': '',
+ 'moto highway rider': 'motorbike',
+ 'mountable barrier 1-ft': '',
+ 'nev shuttle back': 'minibus',
+ 'nev shuttle front': 'minibus',
+ 'nyc bike rack': 'bikerack',
+ 'orange barrel': 'temporary-traffic-cone',
+ 'palm tree': 'palm-tree',
+ 'palmtree 20ft': 'palm-tree',
+ 'palmtree 28ft': 'palm-tree',
+ 'pine tree': 'tree3',
+ 'pink flower 16ft': 'tree3',
+ 'planter flowers': 'dividers-flowers',
+ 'planter with bench': 'bench',
+ 'polaris gem e4': 'tuk-tuk',
+ 'power tower 30ft': '',
+ 'purpendicular right side, blue': '',
+ 'purpendicular right side, red': '',
+ 'purpleleaf plum': 'tree3',
+ 'random trashcan': 'trash-bin',
+ 'red berries 14ft': 'tree3',
+ 'red car': 'sedan-rig',
+ 'red jeep': 'suv-rig',
+ 'rock median (10ft)': '',
+ 'rock median (12ft)': '',
+ 'rock median (4ft)': '',
+ 'rock median (6ft)': '',
+ 'rock median (8ft)': '',
+ 'semi truck': 'box-truck-rig',
+ 'serious man': '',
+ shelter: 'bus-stop',
+ 'shelter roundroof': 'bus-stop',
+ 'sign directory': 'wayfinding',
+ 'silver suv': 'suv-rig',
+ 'small tree': 'tree3',
+ smallnev: 'minibus',
+ 'smartcar 5ft': 'self-driving-cruise-car-rig',
+ 'soundwall (12ft)': '',
+ 'soundwall (8ft)': '',
+ 'soundwall plants (12ft)': '',
+ 'soundwall plants (8ft)': '',
+ 'street light': 'lamp-modern',
+ 'streetcar blue': 'trolley',
+ 'streetcar red 1': 'trolley',
+ 'streetcar red 2': 'trolley',
+ 'streetcar yellow': 'trolley',
+ 'streetlight solar': 'lamp-modern',
+ 'streetlight solar banners 1': 'lamp-modern',
+ 'streetlight solar banners 2': 'lamp-modern',
+ tallgrass: '',
+ 'tallplantbox (10ft)': '',
+ 'tallplantbox (12ft)': 'dividers-bush',
+ 'tallplantbox (4ft)': '',
+ 'tallplantbox (6ft)': '',
+ 'tallplantbox (8ft)': '',
+ 'tallplantbox pedref (10ft)': '',
+ 'tallplantbox pedref (12ft)': '',
+ 'tallplantbox pedref (6ft)': '',
+ 'tallplantbox pedref (8ft)': '',
+ 'telephone pole': 'utility_pole',
+ 'tent bluewhite': '',
+ 'tent veggie': '',
+ 'toward, right park, head in': '',
+ trashcan: 'trash-bin',
+ 'tropical median (4ft)': 'palm-tree',
+ 'two bikes back': '',
+ 'uta bus': 'bus',
+ 'uta lightrail': 'tram',
+ 'uta lightrail rev': 'tram',
+ 'weeds median (4ft)': '',
+ 'weeds median (6ft)': '',
+ 'weeds median (8ft)': '',
+ 'white coup': 'sedan-rig',
+ 'white sedan': 'sedan-rig',
+ 'white truck': 'box-truck-rig',
+ 'yellow sedan': 'sedan-rig'
+};
+
+// Streetplan Helper function to parse O-Tags string into array
+function parseOTags(tags) {
+ if (!tags || tags === '-') return [];
+ return tags.split('", "').map((t) => t.replace(/"/g, '').trim());
+}
+
+// Streetplan Helper function to create clone configuration
+function createCloneConfig(name, tags) {
+ if (!name || name === '-') return null;
+
+ const model = STREETPLAN_OBJECT_MAPPING[name.toLowerCase()];
+ if (!model) return null;
+
+ return {
+ mode: 'fixed', // default to fixed mode
+ model: model,
+ spacing: 15 // default spacing
+ };
+}
+
AFRAME.registerComponent('managed-street', {
schema: {
width: {
@@ -36,53 +236,179 @@ AFRAME.registerComponent('managed-street', {
type: 'boolean',
default: true
},
- justifyWidth: {
- default: 'center',
- type: 'string',
- oneOf: ['center', 'left', 'right']
+ enableAlignment: {
+ type: 'boolean',
+ default: true
},
- justifyLength: {
- default: 'middle',
- type: 'string',
- oneOf: ['middle', 'start', 'end']
+ showGround: {
+ type: 'boolean',
+ default: true
}
},
init: function () {
this.managedEntities = [];
this.pendingEntities = [];
+ this.actualWidth = 0;
// Bind the method to preserve context
this.refreshFromSource = this.refreshFromSource.bind(this);
+ this.onSegmentWidthChanged = this.onSegmentWidthChanged.bind(this);
+
+ if (this.data.enableAlignment && !this.el.hasAttribute('street-align')) {
+ this.el.setAttribute('street-align', '');
+ }
+ if (this.data.showGround && !this.el.hasAttribute('street-ground')) {
+ this.el.setAttribute('street-ground', '');
+ }
+ if (!this.el.hasAttribute('street-label')) {
+ this.el.setAttribute('street-label', '');
+ }
+
+ this.setupEventDispatcher();
+
+ setTimeout(() => {
+ this.attachListenersToExistingSegments();
+ }, 0);
},
- setupMutationObserver: function () {
- // Create mutation observer
+ attachListenersToExistingSegments: function () {
+ const segments = this.el.querySelectorAll('[street-segment]');
+ segments.forEach((segment) => {
+ console.log('Attaching width change listener to existing segment');
+ segment.addEventListener(
+ 'segment-width-changed',
+ this.onSegmentWidthChanged
+ );
+ });
+ },
+ /**
+ * Inserts a new street segment at the specified index
+ * @param {number} index - The index at which to insert the new segment
+ * @param {string} type - The segment type (e.g., 'drive-lane', 'bike-lane')
+ * @param {Object} [segmentObject] - Optional configuration object for the segment
+ * @returns {Element} The created segment element
+ */
+ insertSegment: function (index, type, segmentObject = null) {
+ // Validate index
+ if (index < 0 || index > this.managedEntities.length) {
+ console.error('[managed-street] Invalid index for insertion:', index);
+ return;
+ }
+
+ // Create new segment entity
+ const segmentEl = document.createElement('a-entity');
+
+ // Get default properties for this segment type from STREET.types
+ const defaultProps = window.STREET.types[type] || {};
+
+ // Set up basic segment properties, merging defaults with any provided custom properties
+ const segmentProps = {
+ type: type,
+ width: segmentObject?.width || defaultProps.width || 3,
+ length: this.data.length,
+ level: segmentObject?.level ?? defaultProps.level ?? 0,
+ direction:
+ segmentObject?.direction || defaultProps.direction || 'outbound',
+ color:
+ segmentObject?.color ||
+ defaultProps.color ||
+ window.STREET.colors.white,
+ surface: segmentObject?.surface || defaultProps.surface || 'asphalt'
+ };
+
+ // Set the segment component with properties
+ segmentEl.setAttribute('street-segment', segmentProps);
+
+ // Set the layer name for the segment
+ const layerName = segmentObject?.name || `${type} • default`;
+ segmentEl.setAttribute('data-layer-name', layerName);
+
+ // If custom segment object is provided, wait for segment to load then generate its components
+ if (segmentObject) {
+ segmentEl.addEventListener('loaded', () => {
+ // Use the generateComponentsFromSegmentObject method from street-segment component
+ const streetSegmentComponent = segmentEl.components['street-segment'];
+ if (streetSegmentComponent) {
+ streetSegmentComponent.generateComponentsFromSegmentObject(
+ segmentObject
+ );
+ }
+ });
+ }
+
+ // Insert the segment at the specified index in the DOM
+ const referenceNode = this.managedEntities[index] ?? null;
+ this.el.insertBefore(segmentEl, referenceNode);
+
+ // Wait for the segment to be fully loaded
+ segmentEl.addEventListener('loaded', () => {
+ // Refresh the managed entities list
+ this.refreshManagedEntities();
+
+ // Update the total width
+ const totalWidth = this.managedEntities.reduce((sum, segment) => {
+ return sum + (segment.getAttribute('street-segment').width || 0);
+ }, 0);
+ this.el.setAttribute('managed-street', 'width', totalWidth);
+
+ // If we have a previous segment, check if we need to add stripe separators
+ // TODO: Check striping here in the future
+ });
+
+ return segmentEl;
+ },
+ setupEventDispatcher: function () {
+ // Remove if existing mutation observer
if (this.observer) {
this.observer.disconnect();
}
- this.observer = new MutationObserver((mutations) => {
- let needsReflow = false;
+ // Mutation observer for add/remove
+ const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
- if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
- // Check if any of the removed nodes were street segments
- mutation.removedNodes.forEach((node) => {
- if (node.hasAttribute && node.hasAttribute('street-segment')) {
- needsReflow = true;
- }
+ if (mutation.type === 'childList') {
+ const addedSegments = Array.from(mutation.addedNodes).filter(
+ (node) => node.hasAttribute && node.hasAttribute('street-segment')
+ );
+ const removedSegments = Array.from(mutation.removedNodes).filter(
+ (node) => node.hasAttribute && node.hasAttribute('street-segment')
+ );
+
+ // Add listeners to new segments
+ addedSegments.forEach((segment) => {
+ segment.addEventListener(
+ 'segment-width-changed',
+ this.onSegmentWidthChanged
+ );
+ });
+
+ // Remove listeners from removed segments
+ removedSegments.forEach((segment) => {
+ segment.removeEventListener(
+ 'segment-width-changed',
+ this.onSegmentWidthChanged
+ );
});
+
+ if (addedSegments.length || removedSegments.length) {
+ this.el.emit('segments-changed', {
+ changeType: 'structure',
+ added: addedSegments,
+ removed: removedSegments
+ });
+ }
}
});
-
- // If segments were removed, trigger reflow
- if (needsReflow) {
- this.refreshManagedEntities();
- this.applyJustification();
- this.createOrUpdateJustifiedDirtBox();
- }
});
- // Start observing the managed-street element
- this.observer.observe(this.el, {
- childList: true // watch for child additions/removals
+ observer.observe(this.el, { childList: true });
+ },
+ onSegmentWidthChanged: function (event) {
+ console.log('segment width changed handler called', event);
+ this.el.emit('segments-changed', {
+ changeType: 'property',
+ property: 'width',
+ segment: event.target,
+ oldValue: event.detail.oldWidth,
+ newValue: event.detail.newWidth
});
},
update: function (oldData) {
@@ -95,35 +421,21 @@ AFRAME.registerComponent('managed-street', {
}
const dataDiffKeys = Object.keys(dataDiff);
- if (
- dataDiffKeys.length === 1 &&
- (dataDiffKeys.includes('justifyWidth') ||
- dataDiffKeys.includes('justifyLength'))
- ) {
- this.refreshManagedEntities();
- this.applyJustification();
- this.createOrUpdateJustifiedDirtBox();
- }
-
- if (dataDiffKeys.includes('width')) {
- this.createOrUpdateJustifiedDirtBox();
- }
if (dataDiffKeys.includes('length')) {
this.refreshManagedEntities();
this.applyLength();
- this.createOrUpdateJustifiedDirtBox();
}
// if the value of length changes, then we need to update the length of all the child objects
// we need to get a list of all the child objects whose length we need to change
+ this.setupEventDispatcher();
},
refreshFromSource: function () {
const data = this.data;
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);
+ this.loadAndParseStreetplanURL(data.sourceValue);
} else if (data.sourceType === 'json-blob') {
// if data.sourceValue is a string convert string to object for parsing but keep string for saving
if (typeof data.sourceValue === 'string') {
@@ -145,98 +457,16 @@ AFRAME.registerComponent('managed-street', {
segmentEl.setAttribute('street-segment', 'length', streetLength);
});
},
- applyJustification: function () {
- const data = this.data;
- const segmentEls = this.managedEntities;
- const streetWidth = data.width;
- const streetLength = data.length;
-
- // set starting xPosition for width justification
- let xPosition = 0; // default for left justified
- if (data.justifyWidth === 'center') {
- xPosition = -streetWidth / 2;
- }
- if (data.justifyWidth === 'right') {
- xPosition = -streetWidth;
- }
- // set z value for length justification
- let zPosition = 0; // default for middle justified
- if (data.justifyLength === 'start') {
- zPosition = -streetLength / 2;
- }
- if (data.justifyLength === 'end') {
- zPosition = streetLength / 2;
- }
-
- segmentEls.forEach((segmentEl) => {
- if (!segmentEl.getAttribute('street-segment')) {
- return;
- }
- const segmentWidth = segmentEl.getAttribute('street-segment').width;
- const yPosition = segmentEl.getAttribute('position').y;
- xPosition += segmentWidth / 2;
- segmentEl.setAttribute(
- 'position',
- `${xPosition} ${yPosition} ${zPosition}`
- );
- xPosition += segmentWidth / 2;
- });
- },
refreshManagedEntities: function () {
// create a list again of the managed entities
this.managedEntities = Array.from(
this.el.querySelectorAll('[street-segment]')
);
- this.setupMutationObserver();
- },
- createOrUpdateJustifiedDirtBox: function () {
- const data = this.data;
- const streetWidth = data.width;
- if (!streetWidth) {
- return;
- }
- const streetLength = data.length;
- if (!this.justifiedDirtBox) {
- // try to find an existing dirt box
- this.justifiedDirtBox = this.el.querySelector('.dirtbox');
- }
- if (!this.justifiedDirtBox) {
- // create new brown box to represent ground underneath street
- const dirtBox = document.createElement('a-box');
- dirtBox.classList.add('dirtbox');
- this.el.append(dirtBox);
- 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);
- this.justifiedDirtBox.setAttribute('depth', streetLength - 0.2); // depth is length - 0.1 on each side
-
- // set starting xPosition for width justification
- let xPosition = 0; // default for center justified
- if (data.justifyWidth === 'left') {
- xPosition = streetWidth / 2;
- }
- if (data.justifyWidth === 'right') {
- xPosition = -streetWidth / 2;
- }
-
- // set z value for length justification
- let zPosition = 0; // default for middle justified
- if (data.justifyLength === 'start') {
- zPosition = -streetLength / 2;
- }
- if (data.justifyLength === 'end') {
- zPosition = streetLength / 2;
- }
-
- this.justifiedDirtBox.setAttribute(
- 'position',
- `${xPosition} -1 ${zPosition}`
- );
+ // calculate actual width
+ this.actualWidth = this.managedEntities.reduce((sum, segment) => {
+ return sum + (segment.getAttribute('street-segment')?.width || 0);
+ }, 0);
+ console.log('actual width', this.actualWidth);
},
parseStreetObject: function (streetObject) {
// reset and delete all existing entities
@@ -252,6 +482,7 @@ AFRAME.registerComponent('managed-street', {
for (let i = 0; i < streetObject.segments.length; i++) {
const segment = streetObject.segments[i];
+ const previousSegment = streetObject.segments[i - 1];
const segmentEl = document.createElement('a-entity');
this.el.appendChild(segmentEl);
@@ -267,13 +498,211 @@ AFRAME.registerComponent('managed-street', {
segmentEl.setAttribute('data-layer-name', segment.name);
// wait for street-segment to be loaded, then generate components from segment object
segmentEl.addEventListener('loaded', () => {
+ if (!segment.generated?.striping) {
+ const stripingVariant = this.getStripingFromSegments(
+ previousSegment,
+ segment
+ );
+ if (stripingVariant) {
+ // Only add striping if variant is not null
+ if (!segment.generated) {
+ segment.generated = {};
+ }
+ segment.generated.striping = [
+ {
+ striping: stripingVariant,
+ length: streetObject.length,
+ segmentWidth: segment.width
+ }
+ ];
+ }
+ }
segmentEl.components[
'street-segment'
].generateComponentsFromSegmentObject(segment);
- this.applyJustification();
});
}
},
+ loadAndParseStreetplanURL: async function (streetplanURL) {
+ console.log(
+ '[managed-street] loader',
+ 'sourceType: `streetplan-url`, loading from',
+ streetplanURL
+ );
+
+ try {
+ const response = await fetch(streetplanURL);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const streetplanData = await response.json();
+ const boulevard = streetplanData.project['My Street']['Boulevard Alt 1'];
+
+ const streetLength =
+ parseFloat(streetplanData.project['My Street'].LengthMiles) *
+ 5280 *
+ 0.3048 || 100; // Convert miles to meters
+ // Convert StreetPlan format to managed-street format
+ const streetObject = {
+ name: streetplanData.project.ProjectName,
+ width: 0, // Will be calculated from segments
+ length: streetLength,
+ segments: []
+ };
+
+ // Process streetplan segments
+ const segments = boulevard.segments;
+ for (const segmentKey in segments) {
+ const segment = segments[segmentKey];
+
+ // Skip Buildings and Setback segments
+ if (segment.Type === 'Buildings' || segment.Type === 'Setback') {
+ continue;
+ }
+
+ const segmentWidth = parseFloat(segment.width) * 0.3048; // Convert feet to meters
+ streetObject.width += segmentWidth;
+
+ // Convert streetplan segment type based on your schema
+ let segmentType = 'drive-lane'; // Default type
+ let segmentDirection = 'inbound';
+
+ // convert from streetplan type to managed street default type
+ switch (segment.Type) {
+ case 'BikesPaths':
+ segmentType = 'bike-lane';
+ break;
+ case 'Walkways':
+ segmentType = 'sidewalk';
+ break;
+ case 'Transit':
+ segmentType = 'bus-lane';
+ break;
+ case 'Median/Buffer':
+ segmentType = 'divider';
+ break;
+ case 'Curbside':
+ segmentType = 'divider';
+ break;
+ case 'Gutter':
+ segmentType = 'parking-lane';
+ break;
+ case 'Furniture':
+ segmentType = 'sidewalk-tree';
+ break;
+ // Add more type mappings as needed
+ }
+
+ // Determine direction based on segment data
+ if (segment.Direction === 'Coming') {
+ segmentDirection = 'inbound';
+ } else if (segment.Direction === 'Going') {
+ segmentDirection = 'outbound';
+ }
+
+ // Map the material using the STREETPLAN_MATERIAL_MAPPING, fallback to 'asphalt' if not found
+ const material = segment.Material?.toLowerCase() || '';
+ const mappedSurface =
+ STREETPLAN_MATERIAL_MAPPING[material]?.surface || 'asphalt';
+ const mappedColor = STREETPLAN_MATERIAL_MAPPING[material]?.color;
+
+ // Map the O-Tags to clone configurations
+ const generated = {};
+ const clones = [];
+ // Process O1, O2, O3 configurations
+ ['O1', 'O2', 'O3'].forEach((prefix) => {
+ const name = segment[`${prefix}-Name`];
+ const tags = parseOTags(segment[`${prefix}-Tags`]);
+ const cloneConfig = createCloneConfig(name, tags);
+ if (cloneConfig) {
+ clones.push(cloneConfig);
+ }
+ });
+ if (clones.length > 0) {
+ generated.clones = clones;
+ }
+
+ streetObject.segments.push({
+ type: segmentType,
+ width: segmentWidth,
+ name: segment.title,
+ level: parseFloat(segment.MaterialH) === 0.5 ? 1 : 0,
+ direction: segmentDirection,
+ color: mappedColor || window.STREET.types[segmentType]?.color,
+ surface: mappedSurface,
+ generated: clones.length > 0 ? generated : undefined
+ });
+ }
+
+ // Parse the street object
+ this.parseStreetObject(streetObject);
+ } catch (error) {
+ console.error('[managed-street] loader', 'Loading Error:', error);
+ STREET.notify.warningMessage(
+ 'Error loading StreetPlan data: ' + error.message
+ );
+ }
+ },
+
+ getStripingFromSegments: function (previousSegment, currentSegment) {
+ if (!previousSegment || !currentSegment) {
+ return null;
+ }
+
+ // Valid lane types that should have striping
+ const validLaneTypes = [
+ 'drive-lane',
+ 'bus-lane',
+ 'bike-lane',
+ 'parking-lane'
+ ];
+
+ // Only add striping between valid lane types
+ if (
+ !validLaneTypes.includes(previousSegment.type) ||
+ !validLaneTypes.includes(currentSegment.type)
+ ) {
+ return null;
+ }
+
+ // Default to solid line
+ let variantString = 'solid-stripe';
+
+ // Check for opposite directions
+ if (
+ previousSegment.direction !== currentSegment.direction &&
+ previousSegment.direction !== 'none' &&
+ currentSegment.direction !== 'none'
+ ) {
+ variantString = 'solid-doubleyellow';
+
+ // Special case for bike lanes
+ if (
+ currentSegment.type === 'bike-lane' &&
+ previousSegment.type === 'bike-lane'
+ ) {
+ variantString = 'short-dashed-stripe-yellow';
+ }
+ } else {
+ // Same direction cases
+ if (currentSegment.type === previousSegment.type) {
+ variantString = 'dashed-stripe';
+ }
+
+ // Drive lane and turn lane combination would go here if needed
+ }
+
+ // Special case for parking lanes - use dashed line between parking and drive lanes
+ if (
+ currentSegment.type === 'parking-lane' ||
+ previousSegment.type === 'parking-lane'
+ ) {
+ variantString = 'solid-stripe';
+ }
+
+ return variantString;
+ },
loadAndParseStreetmixURL: async function (streetmixURL) {
const data = this.data;
const streetmixAPIURL = streetmixUtils.streetmixUserToAPI(streetmixURL);
@@ -338,8 +767,6 @@ 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);
});
} catch (error) {
@@ -456,7 +883,7 @@ function getSeparatorMixinId(previousSegment, currentSegment) {
currentSegment.type === 'parking-lane' ||
previousSegment.type === 'parking-lane'
) {
- variantString = 'invisible';
+ variantString = 'solid-stripe';
}
return variantString;
diff --git a/src/components/polygon-offset.js b/src/components/polygon-offset.js
new file mode 100644
index 000000000..030cebe99
--- /dev/null
+++ b/src/components/polygon-offset.js
@@ -0,0 +1,80 @@
+// Component to fix z-fighting by adding polygon offset to meshes
+AFRAME.registerComponent('polygon-offset', {
+ schema: {
+ // Negative values move fragments closer to camera
+ factor: { type: 'number', default: -2 },
+ units: { type: 'number', default: -2 }
+ },
+
+ init: function () {
+ // Initial update when mesh is loaded
+ const mesh = this.el.getObject3D('mesh');
+ if (mesh) {
+ this.applyPolygonOffsetToObject(mesh);
+ }
+
+ // Listen for model-loaded event
+ this.el.addEventListener('model-loaded', (evt) => {
+ const mesh = this.el.getObject3D('mesh');
+ this.applyPolygonOffsetToObject(mesh);
+ });
+ },
+
+ applyPolygonOffsetToObject: function (object3D) {
+ if (!object3D) return;
+
+ object3D.traverse((obj) => {
+ if (obj.isMesh) {
+ if (Array.isArray(obj.material)) {
+ obj.material.forEach((material) => {
+ this.updateMaterial(material);
+ });
+ } else {
+ this.updateMaterial(obj.material);
+ }
+ }
+ });
+ },
+
+ updateMaterial: function (material) {
+ if (!material) return;
+
+ material.polygonOffset = true;
+ material.polygonOffsetFactor = this.data.factor;
+ material.polygonOffsetUnits = this.data.units;
+
+ // Ensure material updates
+ material.needsUpdate = true;
+ },
+
+ update: function (oldData) {
+ // Handle property updates
+ const mesh = this.el.getObject3D('mesh');
+ if (mesh) {
+ this.applyPolygonOffsetToObject(mesh);
+ }
+ },
+
+ remove: function () {
+ const mesh = this.el.getObject3D('mesh');
+ if (mesh) {
+ mesh.traverse((obj) => {
+ if (obj.isMesh) {
+ if (Array.isArray(obj.material)) {
+ obj.material.forEach((material) => {
+ material.polygonOffset = false;
+ material.polygonOffsetFactor = 0;
+ material.polygonOffsetUnits = 0;
+ material.needsUpdate = true;
+ });
+ } else if (obj.material) {
+ obj.material.polygonOffset = false;
+ obj.material.polygonOffsetFactor = 0;
+ obj.material.polygonOffsetUnits = 0;
+ obj.material.needsUpdate = true;
+ }
+ }
+ });
+ }
+ }
+});
diff --git a/src/components/street-align.js b/src/components/street-align.js
new file mode 100644
index 000000000..4a214a5ec
--- /dev/null
+++ b/src/components/street-align.js
@@ -0,0 +1,96 @@
+/* global AFRAME */
+
+AFRAME.registerComponent('street-align', {
+ dependencies: ['managed-street'],
+ schema: {
+ width: {
+ default: 'center',
+ type: 'string',
+ oneOf: ['center', 'left', 'right']
+ },
+ length: {
+ default: 'start',
+ type: 'string',
+ oneOf: ['middle', 'start', 'end']
+ }
+ },
+
+ init: function () {
+ // Listen for any segment changes from managed-street
+ this.realignStreet = this.realignStreet.bind(this);
+ this.el.addEventListener('segments-changed', this.realignStreet);
+
+ // wait for all components, including managed-street to be initialized
+ setTimeout(() => {
+ this.realignStreet();
+ }, 0);
+ },
+
+ update: function (oldData) {
+ const data = this.data;
+ const diff = AFRAME.utils.diff(oldData, data);
+
+ // Only realign if width or length alignment changed
+ if (diff.width !== undefined || diff.length !== undefined) {
+ this.el.emit('alignment-changed', {
+ changeType: 'alignment',
+ oldData: oldData,
+ newData: data
+ });
+ this.realignStreet();
+ }
+ },
+
+ realignStreet: function () {
+ const data = this.data;
+
+ // Get all segments
+ const segments = Array.from(this.el.querySelectorAll('[street-segment]'));
+ if (segments.length === 0) return;
+
+ // Calculate total width
+ const totalWidth = segments.reduce((sum, segment) => {
+ return sum + (segment.getAttribute('street-segment')?.width || 0);
+ }, 0);
+ console.log('total width', totalWidth);
+
+ // Get street length from managed-street component
+ const streetLength = this.el.getAttribute('managed-street')?.length || 0;
+
+ // Calculate starting positions
+ let xPosition = 0;
+ if (data.width === 'center') {
+ xPosition = -totalWidth / 2;
+ } else if (data.width === 'right') {
+ xPosition = -totalWidth;
+ }
+
+ let zPosition = 0;
+ if (data.length === 'start') {
+ zPosition = -streetLength / 2;
+ } else if (data.length === 'end') {
+ zPosition = streetLength / 2;
+ }
+
+ // Position segments
+ segments.forEach((segment) => {
+ const width = segment.getAttribute('street-segment')?.width;
+ const currentPos = segment.getAttribute('position');
+
+ xPosition += width / 2;
+
+ segment.setAttribute('position', {
+ x: xPosition,
+ y: currentPos.y,
+ z: zPosition
+ });
+
+ xPosition += width / 2;
+ });
+ },
+
+ remove: function () {
+ // Clean up event listener
+ this.el.removeEventListener('segments-changed', this.realignStreet);
+ }
+});
diff --git a/src/components/street-generated-clones.js b/src/components/street-generated-clones.js
index b29d5e0ef..419f604be 100644
--- a/src/components/street-generated-clones.js
+++ b/src/components/street-generated-clones.js
@@ -41,6 +41,39 @@ AFRAME.registerComponent('street-generated-clones', {
this.createdEntities.length = 0; // Clear the array
},
+ detach: function () {
+ const commands = [];
+ commands.push([
+ 'componentremove',
+ { entity: this.el, component: this.attrName }
+ ]);
+ let entityObjToPushAtTheEnd = null; // so that the entity is selected after executing the multi command
+ this.createdEntities.forEach((entity) => {
+ const position = entity.getAttribute('position');
+ const rotation = entity.getAttribute('rotation');
+ const entityObj = {
+ parentEl: this.el, // you can also put this.el.id here that way the command is fully json serializable but el currently doesn't have an id
+ mixin: entity.getAttribute('mixin'),
+ 'data-layer-name': entity
+ .getAttribute('data-layer-name')
+ .replace('Cloned Model', 'Detached Model'),
+ components: {
+ position: { x: position.x, y: position.y, z: position.z },
+ rotation: { x: rotation.x, y: rotation.y, z: rotation.z }
+ }
+ };
+ if (AFRAME.INSPECTOR?.selectedEntity === entity) {
+ entityObjToPushAtTheEnd = entityObj;
+ } else {
+ commands.push(['entitycreate', entityObj]);
+ }
+ });
+ if (entityObjToPushAtTheEnd !== null) {
+ commands.push(['entitycreate', entityObjToPushAtTheEnd]);
+ }
+ AFRAME.INSPECTOR.execute('multi', commands);
+ },
+
update: function (oldData) {
// If mode is random or randomFacing and seed is 0, generate a random seed and return,
// the update will be called again because of the setAttribute.
@@ -137,6 +170,7 @@ AFRAME.registerComponent('street-generated-clones', {
clone.classList.add('autocreated');
clone.setAttribute('data-no-transform', '');
clone.setAttribute('data-layer-name', 'Cloned Model • ' + mixinId);
+ clone.setAttribute('data-parent-component', this.attrName);
this.el.appendChild(clone);
this.createdEntities.push(clone);
diff --git a/src/components/street-generated-label.js b/src/components/street-generated-label.js
deleted file mode 100644
index 797f6a998..000000000
--- a/src/components/street-generated-label.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* global AFRAME */
-
-// WIP make managed street labels from canvas
-// assumes existing canvas with id label-canvas
-//
-//
- // AFRAME.registerComponent('draw-canvas', {
- // schema: {
- // myCanvas: { type: 'string' },
- // managedStreet: { type: 'string' } // json of managed street children
- // },
- // init: function () {
- // // const objects = this.data.managedStreet.children;
- // const objects = JSON.parse(this.data.managedStreet).children;
- // this.canvas = document.getElementById(this.data);
- // this.ctx = this.canvas.getContext('2d');
- // // Calculate total width from all objects
- // const totalWidth = objects.reduce((sum, obj) => sum + obj.width, 0);
- // ctx = this.ctx;
- // canvas = this.canvas;
- // // Set up canvas styling
- // ctx.fillStyle = '#ffffff';
- // ctx.fillRect(0, 0, canvas.width, canvas.height);
- // ctx.font = '24px Arial';
- // ctx.textAlign = 'center';
- // ctx.textBaseline = 'middle';
- // // Track current x position
- // let currentX = 0;
- // // Draw each segment
- // objects.forEach((obj, index) => {
- // // Calculate proportional width for this segment
- // const segmentWidth = (obj.width / totalWidth) * canvas.width;
- // // Draw segment background with alternating colors
- // ctx.fillStyle = index % 2 === 0 ? '#f0f0f0' : '#e0e0e0';
- // ctx.fillRect(currentX, 0, segmentWidth, canvas.height);
- // // Draw segment border
- // ctx.strokeStyle = '#999999';
- // ctx.beginPath();
- // ctx.moveTo(currentX, 0);
- // ctx.lineTo(currentX, canvas.height);
- // ctx.stroke();
- // // Draw centered label
- // ctx.fillStyle = '#000000';
- // const centerX = currentX + (segmentWidth / 2);
- // const centerY = canvas.height / 2;
- // // Format width number for display
- // const label = obj.width.toLocaleString();
- // // Draw label with background for better readability
- // const textMetrics = ctx.measureText(label);
- // const textHeight = 30; // Approximate height of text
- // const padding = 10;
- // // Draw text background
- // ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
- // ctx.fillRect(
- // centerX - (textMetrics.width / 2) - padding,
- // centerY - (textHeight / 2) - padding,
- // textMetrics.width + (padding * 2),
- // textHeight + (padding * 2)
- // );
- // // Draw text
- // ctx.fillStyle = '#000000';
- // ctx.fillText(label, centerX, centerY);
- // // Update x position for next segment
- // currentX += segmentWidth;
- // });
- // // Draw final border
- // ctx.strokeStyle = '#999999';
- // ctx.beginPath();
- // ctx.moveTo(canvas.width, 0);
- // ctx.lineTo(canvas.width, canvas.height);
- // ctx.stroke();
- // // Draw on canvas...
- // }
- // });
- //
- }
-});
diff --git a/src/components/street-generated-pedestrians.js b/src/components/street-generated-pedestrians.js
index 5fef3cb6b..4c99aefba 100644
--- a/src/components/street-generated-pedestrians.js
+++ b/src/components/street-generated-pedestrians.js
@@ -45,6 +45,38 @@ AFRAME.registerComponent('street-generated-pedestrians', {
this.createdEntities.forEach((entity) => entity.remove());
this.createdEntities.length = 0;
},
+ detach: function () {
+ const commands = [];
+ commands.push([
+ 'componentremove',
+ { entity: this.el, component: this.attrName }
+ ]);
+ let entityObjToPushAtTheEnd = null; // so that the entity is selected after executing the multi command
+ this.createdEntities.forEach((entity) => {
+ const position = entity.getAttribute('position');
+ const rotation = entity.getAttribute('rotation');
+ const entityObj = {
+ parentEl: this.el, // you can also put this.el.id here that way the command is fully json serializable but el currently doesn't have an id
+ mixin: entity.getAttribute('mixin'),
+ 'data-layer-name': entity
+ .getAttribute('data-layer-name')
+ .replace('Cloned Pedestrian', 'Detached Pedestrian'),
+ components: {
+ position: { x: position.x, y: position.y, z: position.z },
+ rotation: { x: rotation.x, y: rotation.y, z: rotation.z }
+ }
+ };
+ if (AFRAME.INSPECTOR?.selectedEntity === entity) {
+ entityObjToPushAtTheEnd = entityObj;
+ } else {
+ commands.push(['entitycreate', entityObj]);
+ }
+ });
+ if (entityObjToPushAtTheEnd !== null) {
+ commands.push(['entitycreate', entityObjToPushAtTheEnd]);
+ }
+ AFRAME.INSPECTOR.execute('multi', commands);
+ },
update: function (oldData) {
const data = this.data;
@@ -109,7 +141,8 @@ AFRAME.registerComponent('street-generated-pedestrians', {
// Add metadata
pedestrian.classList.add('autocreated');
pedestrian.setAttribute('data-no-transform', '');
- pedestrian.setAttribute('data-layer-name', 'Generated Pedestrian');
+ pedestrian.setAttribute('data-layer-name', 'Cloned Pedestrian');
+ pedestrian.setAttribute('data-parent-component', this.attrName);
this.createdEntities.push(pedestrian);
}
diff --git a/src/components/street-generated-stencil.js b/src/components/street-generated-stencil.js
index b78e33969..542b6dfd1 100644
--- a/src/components/street-generated-stencil.js
+++ b/src/components/street-generated-stencil.js
@@ -99,6 +99,38 @@ AFRAME.registerComponent('street-generated-stencil', {
this.createdEntities.forEach((entity) => entity.remove());
this.createdEntities.length = 0; // Clear the array
},
+ detach: function () {
+ const commands = [];
+ commands.push([
+ 'componentremove',
+ { entity: this.el, component: this.attrName }
+ ]);
+ let entityObjToPushAtTheEnd = null; // so that the entity is selected after executing the multi command
+ this.createdEntities.forEach((entity) => {
+ const position = entity.getAttribute('position');
+ const rotation = entity.getAttribute('rotation');
+ const entityObj = {
+ parentEl: this.el, // you can also put this.el.id here that way the command is fully json serializable but el currently doesn't have an id
+ mixin: entity.getAttribute('mixin'),
+ 'data-layer-name': entity
+ .getAttribute('data-layer-name')
+ .replace('Cloned Model', 'Detached Model'),
+ components: {
+ position: { x: position.x, y: position.y, z: position.z },
+ rotation: { x: rotation.x, y: rotation.y, z: rotation.z }
+ }
+ };
+ if (AFRAME.INSPECTOR?.selectedEntity === entity) {
+ entityObjToPushAtTheEnd = entityObj;
+ } else {
+ commands.push(['entitycreate', entityObj]);
+ }
+ });
+ if (entityObjToPushAtTheEnd !== null) {
+ commands.push(['entitycreate', entityObjToPushAtTheEnd]);
+ }
+ AFRAME.INSPECTOR.execute('multi', commands);
+ },
update: function (oldData) {
const data = this.data;
@@ -166,6 +198,8 @@ AFRAME.registerComponent('street-generated-stencil', {
clone.classList.add('autocreated');
clone.setAttribute('data-no-transform', '');
clone.setAttribute('data-layer-name', `Cloned Model • ${stencilName}`);
+ clone.setAttribute('data-parent-component', this.attrName);
+ clone.setAttribute('polygon-offset', { factor: -2, units: -2 });
this.el.appendChild(clone);
this.createdEntities.push(clone);
diff --git a/src/components/street-generated-striping.js b/src/components/street-generated-striping.js
index 8714eb722..f9f43d50c 100644
--- a/src/components/street-generated-striping.js
+++ b/src/components/street-generated-striping.js
@@ -7,7 +7,18 @@ AFRAME.registerComponent('street-generated-striping', {
multiple: true,
schema: {
striping: {
- type: 'string'
+ type: 'string',
+ oneOf: [
+ 'none',
+ 'solid-stripe',
+ 'dashed-stripe',
+ 'short-dashed-stripe',
+ 'short-dashed-stripe-yellow',
+ 'solid-doubleyellow',
+ 'solid-dashed',
+ 'solid-dashed-yellow',
+ 'solid-dashed-yellow-mirror'
+ ]
},
segmentWidth: {
type: 'number'
@@ -43,7 +54,7 @@ AFRAME.registerComponent('street-generated-striping', {
// Clean up old entities
this.remove();
- if (data.striping === 'invisible') {
+ if (!data.striping || data.striping === 'none') {
return;
}
const clone = document.createElement('a-entity');
@@ -75,6 +86,8 @@ AFRAME.registerComponent('street-generated-striping', {
'data-layer-name',
'Cloned Striping • ' + stripingTextureId
);
+ clone.setAttribute('polygon-offset', { factor: -2, units: -2 });
+
this.el.appendChild(clone);
this.createdEntities.push(clone);
},
@@ -106,6 +119,10 @@ AFRAME.registerComponent('street-generated-striping', {
stripingTextureId = 'striping-solid-dashed';
color = '#f7d117';
stripingWidth = 0.4;
+ } else if (stripingName === 'solid-dashed-yellow-mirror') {
+ stripingTextureId = 'striping-solid-dashed-mirror';
+ color = '#f7d117';
+ stripingWidth = 0.4;
}
return { stripingTextureId, repeatY, color, stripingWidth };
}
diff --git a/src/components/street-ground.js b/src/components/street-ground.js
new file mode 100644
index 000000000..d2ec7e901
--- /dev/null
+++ b/src/components/street-ground.js
@@ -0,0 +1,93 @@
+AFRAME.registerComponent('street-ground', {
+ dependencies: ['managed-street', 'street-align'],
+
+ init: function () {
+ this.createOrUpdateDirtbox = this.createOrUpdateDirtbox.bind(this);
+
+ // Listen for any changes from managed-street
+ this.el.addEventListener('segments-changed', this.createOrUpdateDirtbox);
+
+ // Listen for alignment changes
+ this.el.addEventListener('alignment-changed', this.createOrUpdateDirtbox);
+
+ // Create initial dirtbox
+ setTimeout(() => {
+ this.createOrUpdateDirtbox();
+ }, 0);
+ },
+
+ createOrUpdateDirtbox: function () {
+ // Find or create dirtbox element
+ if (!this.dirtbox) {
+ this.dirtbox = this.el.querySelector('.dirtbox');
+ }
+ if (!this.dirtbox) {
+ this.dirtbox = document.createElement('a-box');
+ this.dirtbox.classList.add('autocreated');
+ this.dirtbox.classList.add('.dirtbox');
+ this.el.append(this.dirtbox);
+
+ this.dirtbox.setAttribute(
+ 'material',
+ `color: ${window.STREET.colors.brown};`
+ );
+ this.dirtbox.setAttribute('data-layer-name', 'Underground');
+ this.dirtbox.setAttribute('data-no-transform', '');
+ this.dirtbox.setAttribute('data-ignore-raycaster', '');
+ this.dirtbox.setAttribute('polygon-offset', {
+ factor: 4,
+ units: 4
+ });
+ }
+
+ // Get all segments
+ const segments = Array.from(this.el.querySelectorAll('[street-segment]'));
+ if (segments.length === 0) return;
+
+ const totalWidth = segments.reduce((sum, segment) => {
+ return sum + (segment.getAttribute('street-segment')?.width || 0);
+ }, 0);
+ const streetLength = this.el.getAttribute('managed-street')?.length || 0;
+
+ // Update dirtbox dimensions
+ this.dirtbox.setAttribute('height', 2);
+ this.dirtbox.setAttribute('width', totalWidth);
+ this.dirtbox.setAttribute('depth', streetLength - 0.2);
+
+ // Get alignment from street-align component
+ const streetAlign = this.el.components['street-align'];
+ const alignWidth = streetAlign?.data.width || 'center';
+ const alignLength = streetAlign?.data.length || 'start';
+
+ // Calculate position based on alignment
+ let xPosition = 0;
+ if (alignWidth === 'center') {
+ xPosition = 0;
+ } else if (alignWidth === 'left') {
+ xPosition = totalWidth / 2;
+ } else if (alignWidth === 'right') {
+ xPosition = -totalWidth / 2;
+ }
+
+ let zPosition = 0;
+ if (alignLength === 'start') {
+ zPosition = -streetLength / 2;
+ } else if (alignLength === 'end') {
+ zPosition = streetLength / 2;
+ }
+
+ this.dirtbox.setAttribute('position', `${xPosition} -1 ${zPosition}`);
+ },
+
+ remove: function () {
+ // Clean up
+ if (this.dirtbox) {
+ this.dirtbox.remove();
+ }
+ this.el.removeEventListener('segments-changed', this.createOrUpdateDirtbox);
+ this.el.removeEventListener(
+ 'alignment-changed',
+ this.createOrUpdateDirtbox
+ );
+ }
+});
diff --git a/src/components/street-label.js b/src/components/street-label.js
new file mode 100644
index 000000000..8ce211272
--- /dev/null
+++ b/src/components/street-label.js
@@ -0,0 +1,276 @@
+/* global AFRAME */
+
+AFRAME.registerComponent('street-label', {
+ dependencies: ['managed-street', 'street-align'],
+
+ schema: {
+ enabled: { type: 'boolean', default: true },
+ heightOffset: { type: 'number', default: -2 },
+ rotation: { type: 'vec3', default: { x: 0, y: 0, z: 0 } },
+ zOffset: { type: 'number', default: 1 },
+ labelHeight: { type: 'number', default: 2.5 },
+ baseCanvasWidth: { type: 'number', default: 4096 }
+ },
+
+ init: function () {
+ this.createdEntities = [];
+ this.canvas = null;
+ this.ctx = null;
+
+ // Create and setup canvas
+ this.createAndSetupCanvas();
+
+ // Listen for segment & alignment changes
+ this.updateLabels = this.updateLabels.bind(this);
+ this.el.addEventListener('segments-changed', this.updateLabels);
+ this.el.addEventListener('alignment-changed', this.updateLabels);
+
+ // Handle loading from saved scene
+ setTimeout(() => {
+ if (this.data.enabled) {
+ this.updateLabels();
+ }
+ }, 0);
+ },
+
+ update: function (oldData) {
+ if (oldData && this.data.enabled !== oldData.enabled) {
+ if (!this.data.enabled) {
+ // Hide existing labels
+ this.createdEntities.forEach((entity) => {
+ entity.setAttribute('visible', false);
+ });
+ } else {
+ // Show and update labels
+ this.createdEntities.forEach((entity) => {
+ entity.setAttribute('visible', true);
+ });
+ this.updateLabels();
+ }
+ } else if (this.data.enabled) {
+ this.updateLabels();
+ }
+ },
+
+ updateLabels: function () {
+ const segments = Array.from(this.el.querySelectorAll('[street-segment]'));
+ if (segments.length === 0) return;
+
+ const widthsArray = [];
+ const labelsArray = [];
+
+ segments.forEach((segmentEl) => {
+ const segmentWidth = segmentEl.getAttribute('street-segment')?.width;
+ if (!segmentWidth) return;
+
+ widthsArray.push(segmentWidth);
+ labelsArray.push(segmentEl.getAttribute('data-layer-name') || '');
+ });
+
+ if (widthsArray.length !== labelsArray.length) {
+ console.error('Mismatch between widths and labels arrays');
+ return;
+ }
+
+ const totalWidth = widthsArray.reduce(
+ (sum, width) => sum + parseFloat(width),
+ 0
+ );
+
+ this.updateCanvasDimensions(totalWidth);
+ this.drawLabels(widthsArray, labelsArray, totalWidth);
+ this.createLabelPlane(totalWidth);
+ },
+
+ createAndSetupCanvas: function () {
+ this.canvas = document.createElement('canvas');
+ this.canvas.id = 'street-label-canvas';
+ this.canvas.style.display = 'none';
+ document.body.appendChild(this.canvas);
+ this.ctx = this.canvas.getContext('2d');
+ },
+
+ updateCanvasDimensions: function (totalWidth) {
+ const aspectRatio = totalWidth / this.data.labelHeight;
+
+ this.canvas.width = this.data.baseCanvasWidth;
+ this.canvas.height = Math.round(this.data.baseCanvasWidth / aspectRatio);
+
+ this.fontSize = Math.round(this.canvas.height * 0.18);
+ this.subFontSize = Math.round(this.canvas.height * 0.14);
+ },
+
+ wrapText: function (text, maxWidth) {
+ const words = text.split(' ');
+ const lines = [];
+ let currentLine = words[0];
+
+ for (let i = 1; i < words.length; i++) {
+ const word = words[i];
+ const width = this.ctx.measureText(currentLine + ' ' + word).width;
+
+ if (width < maxWidth) {
+ currentLine += ' ' + word;
+ } else {
+ lines.push(currentLine);
+ currentLine = word;
+ }
+ }
+ lines.push(currentLine);
+ return lines;
+ },
+
+ drawMultilineText: function (lines, x, y, lineHeight) {
+ const totalHeight = (lines.length - 1) * lineHeight;
+ const startY = y - totalHeight / 2;
+
+ lines.forEach((line, index) => {
+ const yPos = startY + index * lineHeight;
+ this.ctx.fillText(line, x, yPos);
+ });
+ },
+
+ drawLabels: function (widthsArray, labelsArray, totalWidth) {
+ const { ctx, canvas } = this;
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ let currentX = 0;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ widthsArray.forEach((width, index) => {
+ const segmentWidth = (parseFloat(width) / totalWidth) * canvas.width;
+ const maxLabelWidth = segmentWidth * 0.9;
+
+ // Draw segment background
+ ctx.fillStyle = index % 2 === 0 ? '#f0f0f0' : '#e0e0e0';
+ ctx.fillRect(currentX, 0, segmentWidth, canvas.height);
+
+ // Draw segment border
+ ctx.strokeStyle = '#999999';
+ ctx.beginPath();
+ ctx.moveTo(currentX, 0);
+ ctx.lineTo(currentX, canvas.height);
+ ctx.stroke();
+
+ // Draw width value
+ ctx.fillStyle = '#000000';
+ ctx.font = `${this.fontSize}px Arial`;
+ const centerX = currentX + segmentWidth / 2;
+ const centerY = canvas.height / 2 - 50;
+
+ const widthText = parseFloat(width).toFixed(1) + 'm';
+ ctx.fillText(widthText, centerX, centerY - this.fontSize * 0.8);
+
+ // Draw wrapped label text
+ if (labelsArray[index]) {
+ ctx.font = `${this.subFontSize}px Arial`;
+ const lines = this.wrapText(labelsArray[index], maxLabelWidth);
+ const lineHeight = this.subFontSize * 1.2;
+ this.drawMultilineText(
+ lines,
+ centerX,
+ centerY + this.fontSize * 0.4 + 75,
+ lineHeight
+ );
+ }
+
+ currentX += segmentWidth;
+ });
+
+ // Draw final border
+ ctx.strokeStyle = '#999999';
+ ctx.beginPath();
+ ctx.moveTo(canvas.width, 0);
+ ctx.lineTo(canvas.width, canvas.height);
+ ctx.stroke();
+ },
+
+ createLabelPlane: function (totalWidth) {
+ // Remove existing entities
+ this.createdEntities.forEach((entity) => {
+ if (entity.parentNode) {
+ entity.parentNode.removeChild(entity);
+ }
+ });
+ this.createdEntities = [];
+
+ // Create new label plane
+ const plane = document.createElement('a-entity');
+
+ plane.setAttribute('geometry', {
+ primitive: 'plane',
+ width: totalWidth,
+ height: this.data.labelHeight
+ });
+
+ plane.setAttribute('material', {
+ src: '#street-label-canvas',
+ transparent: true,
+ alphaTest: 0.5
+ });
+
+ // Get alignment from street-align component
+ const streetAlign = this.el.components['street-align'];
+ const alignWidth = streetAlign?.data.width || 'center';
+ const alignLength = streetAlign?.data.length || 'start';
+
+ // Get street length from managed-street component
+ const streetLength = this.el.getAttribute('managed-street')?.length || 0;
+
+ // Calculate x position based on width alignment
+ let xPosition = 0;
+ if (alignWidth === 'center') {
+ xPosition = 0;
+ } else if (alignWidth === 'left') {
+ xPosition = totalWidth / 2;
+ } else if (alignWidth === 'right') {
+ xPosition = -totalWidth / 2;
+ }
+
+ // Calculate z position based on length alignment
+ let zPosition = this.data.zOffset; // for 'start' alignment
+ if (alignLength === 'middle') {
+ zPosition = streetLength / 2 + this.data.zOffset;
+ } else if (alignLength === 'end') {
+ zPosition = streetLength + this.data.zOffset;
+ }
+
+ plane.setAttribute(
+ 'position',
+ `${xPosition} ${this.data.heightOffset} ${zPosition}`
+ );
+ plane.setAttribute(
+ 'rotation',
+ `${this.data.rotation.x} ${this.data.rotation.y} ${this.data.rotation.z}`
+ );
+ plane.setAttribute('data-layer-name', 'Segment Labels');
+ plane.classList.add('autocreated');
+
+ this.el.appendChild(plane);
+ this.createdEntities.push(plane);
+ },
+
+ remove: function () {
+ // Clean up canvas
+ if (this.canvas && this.canvas.parentNode) {
+ this.canvas.parentNode.removeChild(this.canvas);
+ }
+
+ // Remove created entities
+ this.createdEntities.forEach((entity) => {
+ if (entity.parentNode) {
+ entity.parentNode.removeChild(entity);
+ }
+ });
+ this.createdEntities = [];
+
+ // Remove event listener
+ this.el.removeEventListener('segments-changed', this.updateLabels);
+ this.el.removeEventListener('alignment-changed', this.updateLabels);
+ }
+});
diff --git a/src/components/street-segment.js b/src/components/street-segment.js
index ecac7345c..4bdaed899 100644
--- a/src/components/street-segment.js
+++ b/src/components/street-segment.js
@@ -223,20 +223,23 @@ AFRAME.registerComponent('street-segment', {
if (componentsToGenerate?.stencil?.length > 0) {
componentsToGenerate.stencil.forEach((clone, index) => {
if (clone?.stencils?.length > 0) {
+ // case where there are multiple stencils such as bus-only
this.el.setAttribute(`street-generated-stencil__${index}`, {
stencils: clone.stencils,
length: this.data.length,
spacing: clone.spacing,
- direction: this.data.direction,
- padding: clone.padding
+ direction: clone.direction ?? this.data.direction,
+ padding: clone.padding,
+ cycleOffset: clone.cycleOffset
});
} else {
this.el.setAttribute(`street-generated-stencil__${index}`, {
model: clone.model,
length: this.data.length,
spacing: clone.spacing,
- direction: this.data.direction,
- count: clone.count
+ direction: clone.direction ?? this.data.direction,
+ count: clone.count,
+ cycleOffset: clone.cycleOffset
});
}
});
@@ -252,6 +255,19 @@ AFRAME.registerComponent('street-segment', {
});
});
}
+
+ if (componentsToGenerate?.striping?.length > 0) {
+ componentsToGenerate.striping.forEach((stripe, index) => {
+ this.el.setAttribute(`street-generated-striping__${index}`, {
+ striping: stripe.striping,
+ segmentWidth: this.data.width,
+ length: this.data.length,
+ positionY: stripe.positionY ?? 0.05, // Default to 0.05 if not specified
+ side: stripe.side ?? 'left', // Default to left if not specified
+ facing: stripe.facing ?? 0 // Default to 0 if not specified
+ });
+ });
+ }
},
updateSurfaceFromType: function (typeObject) {
// update color, surface, level from segment type preset
@@ -310,8 +326,11 @@ AFRAME.registerComponent('street-segment', {
this.generateMesh(data);
// if width was changed, trigger re-justification of all street-segments by the managed-street
if (changedProps.includes('width')) {
- this.el.parentNode.components['managed-street'].refreshManagedEntities();
- this.el.parentNode.components['managed-street'].applyJustification();
+ console.log('segment width changed');
+ this.el.emit('segment-width-changed', {
+ oldWidth: oldData.width,
+ newWidth: data.width
+ });
}
},
// for streetmix elevation number values of -1, 0, 1, 2, calculate heightLevel in three.js meters units
diff --git a/src/components/streetplan-loader.js b/src/components/streetplan-loader.js
deleted file mode 100644
index a6f5429b9..000000000
--- a/src/components/streetplan-loader.js
+++ /dev/null
@@ -1,139 +0,0 @@
-/* global AFRAME, XMLHttpRequest */
-import useStore from '../store.js';
-var streetplanUtils = require('../streetplan/streetplan-utils.js');
-
-AFRAME.registerComponent('streetplan-loader', {
- dependencies: ['street'],
- schema: {
- streetplanStreetURL: { type: 'string' },
- streetplanAPIURL: { type: 'string' },
- streetplanEncJSON: { type: 'string' },
- showBuildings: { default: true },
- name: { default: '' },
- synchronize: { default: false }
- },
- streetplanResponseParse: function (streetplanResponseObject) {
- const el = this.el;
- const data = this.data;
- if (!streetplanResponseObject || !streetplanResponseObject.project) {
- console.error('[streetplan-loader] Invalid streetplan data structure');
- return;
- }
- try {
- // convert Streetplan structure to Streetmix-like structure
- const streetData = streetplanUtils.convertStreetStruct(
- streetplanResponseObject
- );
-
- const streetplanSegments = streetData.segments;
- const streetplanName = streetData.name;
- // const projectName = streetData.projectName || streetplanName;
-
- // Update layer name with project and street names
- el.setAttribute('data-layer-name', `StreetPlan • ${streetplanName}`);
-
- const state = useStore.getState();
- if (!state.sceneTitle) {
- state.setSceneTitle(streetplanName);
- }
-
- // Handle buildings if enabled
- if (data.showBuildings) {
- // Find building segments in the full data
- const buildingSegments = streetplanSegments.filter(
- (segment) => segment.type === 'Buildings'
- );
-
- // Set building variants based on side
- const leftBuilding = buildingSegments.find((b) => b.side === 'left');
- const rightBuilding = buildingSegments.find((b) => b.side === 'right');
-
- if (leftBuilding) {
- el.setAttribute('street', 'left', leftBuilding.title);
- }
- if (rightBuilding) {
- el.setAttribute('street', 'right', rightBuilding.title);
- }
- }
- // Set street type
- el.setAttribute('street', 'type', 'streetmixSegmentsMetric');
-
- // Filter out building segments for the main street data if needed
- const finalSegments = data.showBuildings
- ? streetplanSegments.filter((s) => s.type !== 'Buildings')
- : streetplanSegments;
-
- // Set JSON attribute last
- el.setAttribute(
- 'street',
- 'JSON',
- JSON.stringify({ streetmixSegmentsMetric: finalSegments })
- );
- el.emit('streetplan-loader-street-loaded');
- } catch (error) {
- console.error('[streetplan-loader] Error parsing street data:', error);
- el.emit('streetplan-loader-error', { error });
- }
- },
- init: function () {
- this.el.setAttribute('streetplan-loader', 'synchronize', true);
- },
- update: function (oldData) {
- const data = this.data;
-
- // Skip update if synchronization is disabled
- if (!data.synchronize) return;
-
- // Handle URL encoded JSON data
- if (data.streetplanEncJSON) {
- try {
- const streetplanJSON = decodeURIComponent(data.streetplanEncJSON);
- this.streetplanResponseParse(JSON.parse(streetplanJSON));
- } catch (error) {
- console.error('[streetplan-loader] Error parsing encoded JSON:', error);
- this.el.emit('streetplan-loader-error', { error });
- }
- return;
- }
-
- // Skip if URLs haven't changed
- if (
- oldData.streetplanStreetURL === data.streetplanStreetURL &&
- oldData.streetplanAPIURL === data.streetplanAPIURL
- ) {
- return;
- }
-
- // Load from API
- const request = new XMLHttpRequest();
- console.log('[streetplan-loader]', 'GET ' + data.streetplanAPIURL);
-
- request.open('GET', data.streetplanAPIURL, true);
- request.onload = () => {
- if (request.status >= 200 && request.status < 400) {
- try {
- const streetplanResponseObject = JSON.parse(request.response);
- this.streetplanResponseParse(streetplanResponseObject);
- this.el.setAttribute('streetplan-loader', 'synchronize', false);
- } catch (error) {
- console.error(
- '[streetplan-loader] Error parsing API response:',
- error
- );
- this.el.emit('streetplan-loader-error', { error });
- }
- } else {
- const error = new Error(`Server returned status ${request.status}`);
- console.error('[streetplan-loader] API request failed:', error);
- this.el.emit('streetplan-loader-error', { error });
- }
- };
-
- request.onerror = () => {
- const error = new Error('Network request failed');
- console.error('[streetplan-loader] Network error:', error);
- this.el.emit('streetplan-loader-error', { error });
- };
- request.send();
- }
-});
diff --git a/src/editor/components/components/AddLayerPanel/AddLayerPanel.module.scss b/src/editor/components/components/AddLayerPanel/AddLayerPanel.module.scss
index af61b965f..092c558ab 100644
--- a/src/editor/components/components/AddLayerPanel/AddLayerPanel.module.scss
+++ b/src/editor/components/components/AddLayerPanel/AddLayerPanel.module.scss
@@ -74,6 +74,10 @@
.description {
color: rgba(182, 182, 182, 1);
+ max-width: 182px; // Same as image width
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ white-space: normal;
}
}
diff --git a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js
index eed592dd6..6125766d9 100644
--- a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js
+++ b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js
@@ -77,6 +77,32 @@ export function createManagedStreetFromStreetmixURLPrompt(position) {
}
}
+export function createManagedStreetFromStreetplanURLPrompt(position) {
+ // This creates a new Managed Street
+ let streetplanURL = prompt(
+ 'Please enter a StreetPlan URL',
+ 'https://streetplan.net/3dstreet/89474'
+ );
+
+ if (streetplanURL && streetplanURL !== '') {
+ const definition = {
+ id: createUniqueId(),
+ components: {
+ position: position ?? '0 0 0',
+ 'managed-street': {
+ sourceType: 'streetplan-url',
+ sourceValue: streetplanURL,
+ showVehicles: true,
+ showStriping: true,
+ synchronize: true
+ }
+ }
+ };
+
+ AFRAME.INSPECTOR.execute('entitycreate', definition);
+ }
+}
+
export function createManagedStreetFromStreetObject(position, streetObject) {
// This creates a new Managed Street
if (streetObject && streetObject !== '') {
@@ -151,6 +177,50 @@ export function create60ftRightOfWayManagedStreet(position) {
);
}
+export function create40ftRightOfWayManagedStreet(position) {
+ console.log(
+ 'create40ftRightOfWayManagedStreet',
+ defaultStreetObjects.stroad40ftROW
+ );
+ createManagedStreetFromStreetObject(
+ position,
+ defaultStreetObjects.stroad40ftROW
+ );
+}
+
+export function create80ftRightOfWayManagedStreet(position) {
+ console.log(
+ 'create80ftRightOfWayManagedStreet',
+ defaultStreetObjects.stroad80ftROW
+ );
+ createManagedStreetFromStreetObject(
+ position,
+ defaultStreetObjects.stroad80ftROW
+ );
+}
+
+export function create94ftRightOfWayManagedStreet(position) {
+ console.log(
+ 'create94ftRightOfWayManagedStreet',
+ defaultStreetObjects.stroad94ftROW
+ );
+ createManagedStreetFromStreetObject(
+ position,
+ defaultStreetObjects.stroad94ftROW
+ );
+}
+
+export function create150ftRightOfWayManagedStreet(position) {
+ console.log(
+ 'create150ftRightOfWayManagedStreet',
+ defaultStreetObjects.stroad150ftROW
+ );
+ createManagedStreetFromStreetObject(
+ position,
+ defaultStreetObjects.stroad150ftROW
+ );
+}
+
export function create80ftRightOfWay(position) {
createStreetmixStreet(
position,
diff --git a/src/editor/components/components/AddLayerPanel/defaultStreets.js b/src/editor/components/components/AddLayerPanel/defaultStreets.js
index 098f9f305..76b8a882f 100644
--- a/src/editor/components/components/AddLayerPanel/defaultStreets.js
+++ b/src/editor/components/components/AddLayerPanel/defaultStreets.js
@@ -322,3 +322,1150 @@ export const exampleStreet = {
}
]
};
+
+export const stroad40ftROW = {
+ id: '727dbbf4-692a-48ee-8f99-a056fd60fedd',
+ name: '40ft Right of Way 24ft Road Width',
+ width: 12.192, // Original 40ft converted to 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'dense'
+ }
+ ]
+ }
+ },
+ {
+ id: 'RsLZFtSi3oJH7uufQ5rc4',
+ name: 'Tree Planting Strip',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.61, // Original 2ft
+ direction: 'none',
+ generated: {
+ clones: [
+ {
+ mode: 'fixed',
+ model: 'tree3',
+ spacing: 15
+ }
+ ]
+ }
+ },
+ {
+ id: 'GbEHhCMPmVom_IJK-xIn3',
+ name: 'Inbound Parking',
+ type: 'parking-lane',
+ surface: 'concrete',
+ color: '#dddddd',
+ level: 0,
+ width: 2.134, // Original 7ft
+ 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: 'Drive Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ 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: 'ARosTXeWGXp17QyfZgSKB',
+ name: 'Outbound Parking',
+ type: 'parking-lane',
+ surface: 'concrete',
+ color: '#dddddd',
+ level: 0,
+ width: 2.134, // Original 7ft
+ 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: 'vL9qDNp5neZt32zlZ9ExG',
+ name: 'Tree Planting Strip',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.61, // Original 2ft
+ 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'normal'
+ }
+ ]
+ }
+ }
+ ]
+};
+
+export const stroad80ftROW = {
+ id: 'dea1980d-2a13-481b-b318-9f757ca114f7',
+ name: '80ft Right of Way 56ft Road Width',
+ width: 24.384, // Original 80ft converted to 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'dense'
+ }
+ ]
+ }
+ },
+ {
+ id: 'RsLZFtSi3oJH7uufQ5rc4',
+ name: 'Tree Planting Strip',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ direction: 'none',
+ generated: {
+ clones: [
+ {
+ mode: 'fixed',
+ model: 'tree3',
+ spacing: 15
+ }
+ ]
+ }
+ },
+ {
+ id: 'X2tAKuwUDc728RIPfhJUS',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 8ft
+ 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 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ 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: 'n9A8XDtjRSpgxElVhxoWB',
+ name: 'Inbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ 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: 'O08G5Br9w6vwdomdhUmwk',
+ name: 'Outbound Drive Lane 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ 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: '1w9jjehQwnvBfJeSVOd6M',
+ name: 'Outbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ 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, // Original 8ft
+ 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: '2p_cReSRF4748HV9Fyejr',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 3ft
+ 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'normal'
+ }
+ ]
+ }
+ }
+ ]
+};
+
+export const stroad94ftROW = {
+ id: 'a55d288c-215d-49a2-b67f-3efb9ec9ff41',
+ name: '94ft Right of Way 70ft Road Width',
+ width: 28.651, // Original 94ft converted to 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'dense'
+ }
+ ]
+ }
+ },
+ {
+ id: 'RsLZFtSi3oJH7uufQ5rc4',
+ name: 'Tree Planting Strip',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ direction: 'none',
+ generated: {
+ clones: [
+ {
+ mode: 'fixed',
+ model: 'tree3',
+ spacing: 15
+ }
+ ]
+ }
+ },
+ {
+ id: 'X2tAKuwUDc728RIPfhJUS',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 8ft
+ 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 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ 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: 'n9A8XDtjRSpgxElVhxoWB',
+ name: 'Inbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ 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: 'zUl55HA-DUaJpyQEelUhW',
+ name: 'Center Turn Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ generated: {
+ stencil: [
+ {
+ model: 'left',
+ cycleOffset: 0.6,
+ spacing: 20,
+ direction: 'outbound'
+ },
+ {
+ model: 'left',
+ cycleOffset: 0.4,
+ spacing: 20,
+ direction: 'inbound'
+ }
+ ],
+ striping: [
+ {
+ striping: 'solid-dashed-yellow'
+ },
+ {
+ striping: 'solid-dashed-yellow-mirror',
+ side: 'right'
+ }
+ ]
+ }
+ },
+ {
+ id: 'O08G5Br9w6vwdomdhUmwk',
+ name: 'Outbound Drive Lane 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray:
+ 'sedan-rig, box-truck-rig, self-driving-waymo-car, suv-rig, motorbike',
+ spacing: 7.3,
+ count: 4
+ }
+ ],
+ striping: [
+ {
+ striping: 'none'
+ }
+ ]
+ }
+ },
+ {
+ id: '1w9jjehQwnvBfJeSVOd6M',
+ name: 'Outbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ 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, // Original 8ft
+ 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: '2p_cReSRF4748HV9Fyejr',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 3ft
+ 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, // Original 6ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'normal'
+ }
+ ]
+ }
+ }
+ ]
+};
+
+export const stroad150ftROW = {
+ id: 'f8eeb25c-f68c-4f0b-9435-3f87e6be705a',
+ name: '150ft Right of Way 124ft Road Width',
+ width: 45.72, // Original 150ft converted to meters
+ length: 100,
+ justifyWidth: 'center',
+ justifyLength: 'start',
+ segments: [
+ {
+ id: 'JCWzsLQHmyfDHzQhi9_pU',
+ name: 'Dense Sidewalk',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 2.134, // Original 7ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'dense'
+ }
+ ]
+ }
+ },
+ {
+ id: 'RsLZFtSi3oJH7uufQ5rc4',
+ name: 'Tree Planting Strip',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ direction: 'none',
+ generated: {
+ clones: [
+ {
+ mode: 'fixed',
+ model: 'tree3',
+ spacing: 15
+ }
+ ]
+ }
+ },
+ {
+ id: 'X2tAKuwUDc728RIPfhJUS',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 8ft
+ 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: 'vLBLQS2VraoTL2sJRmD4J',
+ name: 'Inbound Left Turn Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ direction: 'inbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'sedan-rig, suv-rig',
+ spacing: 20,
+ count: 2
+ }
+ ],
+ stencil: [
+ {
+ model: 'turn-lane-left',
+ cycleOffset: 1,
+ spacing: 20
+ }
+ ]
+ }
+ },
+ {
+ id: 'z4gZgzYoM7sQ7mzIV01PC',
+ name: 'Inbound Drive Lane 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'inbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray:
+ 'sedan-rig, self-driving-waymo-car, suv-rig, motorbike',
+ spacing: 7.3,
+ count: 4
+ }
+ ]
+ }
+ },
+ {
+ id: '3a42u-6x8OsoGsI7bjb4z',
+ name: 'Inbound Truck Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'inbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'box-truck-rig, trailer-truck-rig',
+ spacing: 15,
+ count: 2
+ }
+ ]
+ }
+ },
+ {
+ id: 'n9A8XDtjRSpgxElVhxoWB',
+ name: 'Inbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'inbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray:
+ 'sedan-rig, self-driving-waymo-car, suv-rig, motorbike',
+ spacing: 7.3,
+ count: 4
+ }
+ ]
+ }
+ },
+ {
+ id: 'E1UvC71Nkre2H2-hg2gMd',
+ name: 'Inbound Right Turn Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ direction: 'inbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'sedan-rig, suv-rig',
+ spacing: 20,
+ count: 2
+ }
+ ],
+ stencil: [
+ {
+ model: 'turn-lane-right',
+ cycleOffset: 1,
+ spacing: 20
+ }
+ ]
+ }
+ },
+ {
+ id: 'WisaQ2Pfc5K51O8k_Mrnb',
+ name: 'Planted Median',
+ type: 'divider',
+ surface: 'planting-strip',
+ color: '#338833',
+ level: 1,
+ width: 0.61, // Original 2ft
+ direction: 'none',
+ generated: {
+ clones: [
+ {
+ mode: 'fixed',
+ model: 'flowers1',
+ spacing: 3
+ }
+ ]
+ }
+ },
+ {
+ id: 'qvQftgSPmiA7afQles5EK',
+ name: 'Outbound Left Turn Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'sedan-rig, suv-rig',
+ spacing: 20,
+ count: 2
+ }
+ ],
+ stencil: [
+ {
+ model: 'turn-lane-left',
+ cycleOffset: 1,
+ spacing: 20
+ }
+ ]
+ }
+ },
+ {
+ id: 'O08G5Br9w6vwdomdhUmwk',
+ name: 'Outbound Drive Lane 1',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray:
+ 'sedan-rig, self-driving-waymo-car, suv-rig, motorbike',
+ spacing: 7.3,
+ count: 4
+ }
+ ]
+ }
+ },
+ {
+ id: '1w9jjehQwnvBfJeSVOd6M',
+ name: 'Outbound Truck Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'box-truck-rig, trailer-truck-rig',
+ spacing: 15,
+ count: 2
+ }
+ ]
+ }
+ },
+ {
+ id: 'RnfgVJLk2oJv7QTW_s3WR',
+ name: 'Outbound Drive Lane 2',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.353, // Original 11ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray:
+ 'sedan-rig, self-driving-waymo-car, suv-rig, motorbike',
+ spacing: 7.3,
+ count: 4
+ }
+ ]
+ }
+ },
+ {
+ id: 'va_9kr_Dtr9q8ddlzAl02',
+ name: 'Outbound Right Turn Lane',
+ type: 'drive-lane',
+ surface: 'asphalt',
+ color: '#ffffff',
+ level: 0,
+ width: 3.048, // Original 10ft
+ direction: 'outbound',
+ generated: {
+ clones: [
+ {
+ mode: 'random',
+ modelsArray: 'sedan-rig, suv-rig',
+ spacing: 20,
+ count: 2
+ }
+ ],
+ stencil: [
+ {
+ model: 'turn-lane-right',
+ cycleOffset: 1,
+ spacing: 20
+ }
+ ]
+ }
+ },
+ {
+ id: 'ARosTXeWGXp17QyfZgSKB',
+ name: 'Outbound Parking',
+ type: 'parking-lane',
+ surface: 'concrete',
+ color: '#dddddd',
+ level: 0,
+ width: 2.438, // Original 8ft
+ 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: '2p_cReSRF4748HV9Fyejr',
+ name: 'Modern Street Lamp',
+ type: 'sidewalk',
+ surface: 'sidewalk',
+ color: '#ffffff',
+ level: 1,
+ width: 0.914, // Original 3ft
+ 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, // Original 3ft
+ 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: 2.134, // Original 7ft
+ direction: 'none',
+ generated: {
+ pedestrians: [
+ {
+ density: 'normal'
+ }
+ ]
+ }
+ }
+ ]
+};
diff --git a/src/editor/components/components/AddLayerPanel/layersData.js b/src/editor/components/components/AddLayerPanel/layersData.js
index c20b2b372..fd21fd782 100644
--- a/src/editor/components/components/AddLayerPanel/layersData.js
+++ b/src/editor/components/components/AddLayerPanel/layersData.js
@@ -1,13 +1,20 @@
import * as createFunctions from './createLayerFunctions';
export const streetLayersData = [
+ {
+ name: 'Create Intersection',
+ img: '',
+ requiresPro: true,
+ icon: 'ui_assets/cards/icons/3dst24.png',
+ description: 'Create 90º intersection entity.',
+ handlerFunction: createFunctions.createIntersection
+ },
{
name: 'Street from Streetmix URL',
img: 'ui_assets/cards/streetmix.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description:
'Create an additional Streetmix street in your 3DStreet scene without replacing any existing streets.',
- id: 1,
handlerFunction: createFunctions.createStreetmixStreet
},
{
@@ -15,7 +22,6 @@ export const streetLayersData = [
img: 'ui_assets/cards/street-preset-40-24.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description: 'Premade Street 40ft Right of Way / 24ft Roadway Width',
- id: 2,
handlerFunction: createFunctions.create40ftRightOfWay
},
{
@@ -23,7 +29,6 @@ export const streetLayersData = [
img: 'ui_assets/cards/street-preset-60-36.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description: 'Premade Street 60ft Right of Way / 36ft Roadway Width',
- id: 3,
handlerFunction: createFunctions.create60ftRightOfWay
},
{
@@ -31,7 +36,6 @@ export const streetLayersData = [
img: 'ui_assets/cards/street-preset-80-56.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description: 'Premade Street 80ft Right of Way / 56ft Roadway Width',
- id: 4,
handlerFunction: createFunctions.create80ftRightOfWay
},
{
@@ -39,7 +43,6 @@ export const streetLayersData = [
img: 'ui_assets/cards/street-preset-94-70.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description: 'Premade Street 94ft Right of Way / 70ft Roadway Width',
- id: 5,
handlerFunction: createFunctions.create94ftRightOfWay
},
{
@@ -47,38 +50,62 @@ export const streetLayersData = [
img: 'ui_assets/cards/street-preset-150-124.jpg',
icon: 'ui_assets/cards/icons/streetmix24.png',
description: 'Premade Street 150ft Right of Way / 124ft Roadway Width',
- id: 6,
handlerFunction: createFunctions.create150ftRightOfWay
},
- {
- name: 'Create intersection',
- img: '',
- requiresPro: true,
- icon: '',
- description:
- 'Create intersection entity. Parameters of intersection component could be changed in properties panel.',
- id: 7,
- handlerFunction: createFunctions.createIntersection
- },
{
name: '(Beta) Managed Street from Streetmix URL',
img: '',
requiresPro: true,
- icon: '',
+ icon: 'ui_assets/cards/icons/streetmix24.png',
description:
'Create a new street from Streetmix URL using the Managed Street component.',
- id: 8,
handlerFunction: createFunctions.createManagedStreetFromStreetmixURLPrompt
},
+ {
+ name: '(Beta) Managed Street 40ft RoW / 24ft Roadway Width',
+ img: 'ui_assets/cards/street-preset-40-24.jpg',
+ icon: 'ui_assets/cards/icons/3dst24.png',
+ description: 'Premade Street 40ft Right of Way / 24ft Roadway Width',
+ handlerFunction: createFunctions.create40ftRightOfWayManagedStreet
+ },
{
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
+ },
+ {
+ name: '(Beta) Managed Street 80ft RoW / 56ft Roadway Width',
+ img: 'ui_assets/cards/street-preset-80-56.jpg',
+ icon: 'ui_assets/cards/icons/3dst24.png',
+ description: 'Premade Street 80ft Right of Way / 56ft Roadway Width',
+ handlerFunction: createFunctions.create80ftRightOfWayManagedStreet
+ },
+ {
+ name: '(Beta) Managed Street 94ft RoW / 70ft Roadway Width',
+ img: 'ui_assets/cards/street-preset-94-70.jpg',
+ icon: 'ui_assets/cards/icons/3dst24.png',
+ description: 'Premade Street 94ft Right of Way / 70ft Roadway Width',
+ handlerFunction: createFunctions.create94ftRightOfWayManagedStreet
+ },
+ {
+ name: '(Beta) Managed Street 150ft RoW / 124ft Roadway Width',
+ img: 'ui_assets/cards/street-preset-150-124.jpg',
+ icon: 'ui_assets/cards/icons/3dst24.png',
+ description: 'Premade Street 150ft Right of Way / 124ft Roadway Width',
+ handlerFunction: createFunctions.create150ftRightOfWayManagedStreet
+ },
+ {
+ name: '(Beta) Managed Street from Streetplan URL',
+ img: '',
+ requiresPro: true,
+ icon: '',
+ description:
+ 'Create a new street from Streetplan URL using the Managed Street component.',
+ handlerFunction: createFunctions.createManagedStreetFromStreetplanURLPrompt
}
-];
+].map((layer, index) => ({ ...layer, id: index + 1 }));
export const customLayersData = [
{
@@ -88,7 +115,6 @@ export const customLayersData = [
requiresPro: true,
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: createFunctions.createSvgExtrudedEntity
},
{
@@ -98,7 +124,6 @@ export const customLayersData = [
icon: '',
description:
'Create entity with model from path for a glTF (or Glb) file hosted on any publicly accessible HTTP server.',
- id: 2,
handlerFunction: createFunctions.createCustomModel
},
{
@@ -108,7 +133,6 @@ export const customLayersData = [
icon: '',
description:
'Create entity with A-Frame primitive geometry. Geometry type could be changed in properties panel.',
- id: 3,
handlerFunction: createFunctions.createPrimitiveGeometry
},
{
@@ -118,7 +142,6 @@ export const customLayersData = [
icon: 'ui_assets/cards/icons/gallery24.png',
description:
'Place an image such as a sign, reference photo, custom map, etc.',
- id: 4,
handlerFunction: createFunctions.createImageEntity
}
-];
+].map((layer, index) => ({ ...layer, id: index + 1 }));
diff --git a/src/editor/components/components/Sidebar.js b/src/editor/components/components/Sidebar.js
index 4d460c6a6..817a1eeb4 100644
--- a/src/editor/components/components/Sidebar.js
+++ b/src/editor/components/components/Sidebar.js
@@ -15,7 +15,10 @@ import {
ArrowRightIcon,
Object24Icon,
SegmentIcon,
- ManagedStreetIcon
+ ManagedStreetIcon,
+ AutoIcon,
+ ManualIcon,
+ ArrowLeftHookIcon
} from '../../icons';
import GeoSidebar from './GeoSidebar'; // Make sure to create and import this new component
import IntersectionSidebar from './IntersectionSidebar';
@@ -35,6 +38,24 @@ export default class Sidebar extends React.Component {
};
}
+ getParentComponentName = (entity) => {
+ const componentName = entity.getAttribute('data-parent-component');
+ const parentEntity = entity.parentElement;
+ return componentName
+ ? `${parentEntity.getAttribute('data-layer-name') || 'Entity'}:${componentName}`
+ : 'Unknown';
+ };
+
+ fireParentComponentDetach = (entity) => {
+ const componentName = entity.getAttribute('data-parent-component');
+ const parentEntity = entity.parentElement;
+ parentEntity.components[componentName].detach();
+ };
+
+ selectParentEntity = (entity) => {
+ AFRAME.INSPECTOR.selectEntity(entity.parentElement);
+ };
+
onEntityUpdate = (detail) => {
if (detail.entity !== this.props.entity) {
return;
@@ -115,7 +136,48 @@ export default class Sidebar extends React.Component {
{entity.id !== 'reference-layers' &&
!entity.getAttribute('street-segment') ? (
<>
- {!!entity.mixinEls.length && }
+ {entity.classList.contains('autocreated') && (
+ <>
+
+
+
+ >
+ )}
+ {!!entity.mixinEls.length &&
+ !entity.classList.contains('autocreated') && (
+
+ )}
{entity.hasAttribute('data-no-transform') ? (
<>>
) : (
diff --git a/src/editor/icons/icons.jsx b/src/editor/icons/icons.jsx
index e5559ae24..10d15a79a 100644
--- a/src/editor/icons/icons.jsx
+++ b/src/editor/icons/icons.jsx
@@ -1,3 +1,63 @@
+export const ArrowLeftHookIcon = () => (
+
+);
+
+export const ManualIcon = () => (
+
+);
+
+export const AutoIcon = () => (
+
+);
export const Camera32Icon = () => (