Modular custom bowls: polygon pools with per-vertex depths
New bowlpoly element: the rim is a polygon, every vertex a draggable handle with its own depth slider. Inside/outside corners, hips, shallow-to-deep escalators and waterfall drops all come from the same continuous carve (IDW floor blend, quarter-pipe wall transition) — no seams by construction. Auto pool coping (bullnose tube, exported as grindable ledge segments per rim edge) and tile band baked into the ground texture. Ships parks/pool_demo.json (THE POOLROOM) as a premade showcase; ridden end-to-end in test mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7a00c9f9a7
commit
a962ac41d9
14
README.md
14
README.md
@ -19,7 +19,12 @@ persistence (`parks/`, `assets/uploads/`).
|
||||
|
||||
- **TRANSITION** — quarter (escalating radius, lump, vert extension), hip/corner quarter,
|
||||
bank, kicker (tunable concavity), spine, roller/pump bump, mogul, volcano,
|
||||
bowl (square pool or round). **STREET** — funbox, pyramid, ledge, wedge/manny pad
|
||||
bowl (square pool or round), and the **CUSTOM BOWL**: a polygon pool where every rim
|
||||
vertex is a draggable ball with its own depth slider. Inside corners = drag a vertex
|
||||
in, hips = out, shallow-to-deep escalator = ramp the depths, waterfall = two close
|
||||
vertices with different depths — one continuous carve, no seams by construction.
|
||||
Auto **pool coping** (bullnose tube, exported as grindable ledge segments) and a
|
||||
**tile band** baked into the ground texture. **STREET** — funbox, pyramid, ledge, wedge/manny pad
|
||||
(inclined), stairs, island. Click a piece, click the ground. Drag to move, `[` `]`
|
||||
rotate, `D` dupe, `⌫` delete, `⌘Z` undo, optional 0.5m grid snap (THPS style).
|
||||
- **STEEL** — rails are polylines: flatbar, square bar, down rail, kink, double-kink,
|
||||
@ -47,9 +52,10 @@ persistence (`parks/`, `assets/uploads/`).
|
||||
## Park format
|
||||
|
||||
`parks/*.json` — bounds, spawn, `elements` (parametric primitives evaluated by
|
||||
`js/parkmath.js`), `rails`, `gaps`, `letters`, `zones`, `decals`, `props`, optional
|
||||
`underlay`. `parks/paddo.json` is Paddington Skatepark ported from bookquoy — load it,
|
||||
ride it, remix it.
|
||||
`js/parkmath.js`), `rails` (polylines), `gaps`, `letters`, `zones`, `decals`, `props`,
|
||||
optional `underlay`. Premade parks: `parks/paddo.json` (Paddington Skatepark ported from
|
||||
bookquoy) and `parks/pool_demo.json` (THE POOLROOM — custom-bowl showcase, passes PARK
|
||||
CHECK clean). Load them, ride them, remix them.
|
||||
|
||||
## Export → BOOKQUOY
|
||||
|
||||
|
||||
26
js/editor.js
26
js/editor.js
@ -138,7 +138,16 @@ export function initEditor(container) {
|
||||
new THREE.MeshBasicMaterial({ color: 0xffd93b }));
|
||||
m.position.set(x, y, z); m.userData.handle = true; selGrp.add(m); return m;
|
||||
};
|
||||
if (sel.type === 'element') loop(elementOutline(obj));
|
||||
if (sel.type === 'element') {
|
||||
loop(elementOutline(obj));
|
||||
if (obj.kind === 'bowlpoly') { // rim vertices are draggable
|
||||
const c = Math.cos(obj.rot || 0), sn = Math.sin(obj.rot || 0);
|
||||
obj.pts.forEach((p, i) => {
|
||||
const wx = obj.x + p.u * c - p.v * sn, wz = obj.z + p.u * sn + p.v * c;
|
||||
handle(wx, 0.18, wz, true).userData.bowlPt = i;
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (sel.type === 'rail') {
|
||||
const pts = obj.pts.map(p => new THREE.Vector3(p.x, p.y, p.z));
|
||||
selGrp.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), mat));
|
||||
@ -296,6 +305,8 @@ export function initEditor(container) {
|
||||
const hs = ray.intersectObjects(selGrp.children.filter(o => o.userData.handle));
|
||||
if (hs.length && hs[0].object.userData.railPt !== undefined)
|
||||
return { type: 'railPt', i: hs[0].object.userData.railPt };
|
||||
if (hs.length && hs[0].object.userData.bowlPt !== undefined)
|
||||
return { type: 'bowlPt', i: hs[0].object.userData.bowlPt };
|
||||
if (!p) return null;
|
||||
const P = ed.park, x = p.x, z = p.z;
|
||||
// 2. props (real raycast)
|
||||
@ -412,8 +423,8 @@ export function initEditor(container) {
|
||||
if (ed.tool) { if (p) place(p); return; }
|
||||
const hit = pick(ev, p);
|
||||
if (!hit) { ed.select(null); return; }
|
||||
if (hit.type === 'railPt') {
|
||||
drag = { kind: 'railPt', i: hit.i, moved: false };
|
||||
if (hit.type === 'railPt' || hit.type === 'bowlPt') {
|
||||
drag = { kind: hit.type, i: hit.i, moved: false };
|
||||
controls.enabled = false; snapshot(); return;
|
||||
}
|
||||
ed.select({ type: hit.type, id: hit.id });
|
||||
@ -435,6 +446,13 @@ export function initEditor(container) {
|
||||
pt.x = snapV(p.x); pt.z = snapV(p.z);
|
||||
rebuildRails(); rebuildSelection(); return;
|
||||
}
|
||||
if (drag.kind === 'bowlPt') { // world -> bowl-local vertex
|
||||
const c = Math.cos(obj.rot || 0), sn = Math.sin(obj.rot || 0);
|
||||
const du = snapV(p.x) - obj.x, dz = snapV(p.z) - obj.z;
|
||||
obj.pts[drag.i].u = +(du * c + dz * sn).toFixed(2);
|
||||
obj.pts[drag.i].v = +(-du * sn + dz * c).toFixed(2);
|
||||
requestGround(); rebuildRails(); rebuildSelection(); return;
|
||||
}
|
||||
const nx = snapV(p.x + drag.grabDX), nz = snapV(p.z + drag.grabDZ);
|
||||
if (s.type === 'rail') {
|
||||
const dx = nx - obj.pts[0].x, dz = nz - obj.pts[0].z;
|
||||
@ -464,7 +482,7 @@ export function initEditor(container) {
|
||||
const obj = getSelected(); if (!obj) return;
|
||||
obj[key] = value;
|
||||
const t = ed.selection.type;
|
||||
if (t === 'element') requestGround();
|
||||
if (t === 'element') { requestGround(); if (obj.kind === 'bowlpoly') rebuildRails(); }
|
||||
if (t === 'zone' || t === 'decal') rebakeTexture();
|
||||
if (t === 'rail') rebuildRails();
|
||||
if (t === 'prop') rebuildProps();
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// so what you see in the editor is byte-for-byte what the game builds.
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { makeHeightAt, normalizeRail } from './parkmath.js';
|
||||
import { makeHeightAt, normalizeRail, bowlRimWorld } from './parkmath.js';
|
||||
import { makeBuiltinProp } from './props3d.js';
|
||||
|
||||
export const GRID_RES = 0.5, GRID_PAD = 14;
|
||||
@ -40,6 +40,17 @@ export function bakeGroundTexture(park, x0, x1, z0, z1) {
|
||||
g.globalAlpha = zn.opacity ?? 1;
|
||||
g.fill(); g.restore();
|
||||
}
|
||||
// pool detail: tile band + coping line traced around every custom-bowl rim
|
||||
for (const e of park.elements || []) {
|
||||
if (e.kind !== 'bowlpoly' || !e.tile) continue;
|
||||
const rim = bowlRimWorld(e).map(([wx, wz]) => px(wx, wz));
|
||||
for (const [w, color] of [[0.7 * PX, '#7fb4c9'], [0.22 * PX, '#e8e4d8']]) {
|
||||
g.beginPath();
|
||||
rim.forEach(([cx, cz], i) => i ? g.lineTo(cx, cz) : g.moveTo(cx, cz));
|
||||
g.closePath();
|
||||
g.strokeStyle = color; g.lineWidth = w; g.lineJoin = 'round'; g.stroke();
|
||||
}
|
||||
}
|
||||
const jobs = []; // decals load async, re-bake per image
|
||||
for (const d of park.decals || []) {
|
||||
const img = new Image();
|
||||
@ -140,6 +151,23 @@ export function buildRails(park) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// bullnose pool coping around custom-bowl rims
|
||||
const stone = new THREE.MeshStandardMaterial({ color: 0xe8e4d8, roughness: 0.7 });
|
||||
const UP2 = new THREE.Vector3(0, 1, 0);
|
||||
for (const e of park.elements || []) {
|
||||
if (e.kind !== 'bowlpoly' || !e.coping) continue;
|
||||
const rim = bowlRimWorld(e);
|
||||
for (let i = 0; i < rim.length; i++) {
|
||||
const [ax, az] = rim[i], [bx, bz] = rim[(i + 1) % rim.length];
|
||||
const a = new THREE.Vector3(ax, 0.03, az), b = new THREE.Vector3(bx, 0.03, bz);
|
||||
const len = Math.max(a.distanceTo(b), 0.01);
|
||||
const bar = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, len, 8), stone);
|
||||
bar.position.copy(a).add(b).multiplyScalar(0.5);
|
||||
bar.quaternion.setFromUnitVectors(UP2, b.clone().sub(a).normalize());
|
||||
bar.userData.elId = e.id;
|
||||
grp.add(bar);
|
||||
}
|
||||
}
|
||||
return grp;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import { makeHeightAt, elementOutline } from './parkmath.js';
|
||||
|
||||
const STREET_KINDS = new Set(['ledge', 'funbox', 'stairs', 'pyramid', 'wedgepad']);
|
||||
const TRANNY_KINDS = new Set(['quarter', 'spine', 'bowl', 'bank', 'mogul', 'kicker', 'roller', 'corner', 'volcano']);
|
||||
const TRANNY_KINDS = new Set(['quarter', 'spine', 'bowl', 'bank', 'mogul', 'kicker', 'roller', 'corner', 'volcano', 'bowlpoly']);
|
||||
|
||||
export function checkPark(park) {
|
||||
const out = [];
|
||||
|
||||
@ -189,8 +189,62 @@ export const ELEMENT_KINDS = {
|
||||
: Math.abs(u) < e.size && Math.abs(v) < e.size;
|
||||
},
|
||||
},
|
||||
bowlpoly: { // modular pool: polygon rim, per-vertex depths.
|
||||
// Inside corner = concave vertex, outside = convex, escalator = ramp the depths,
|
||||
// waterfall = two close vertices with different depths. One continuous carve.
|
||||
label: 'Custom Bowl',
|
||||
defaults: {
|
||||
pts: [{ u: 3.5, v: 0 }, { u: 2.2, v: 2.8 }, { u: -1.5, v: 3.2 },
|
||||
{ u: -3.8, v: 0.8 }, { u: -3.0, v: -2.4 }, { u: 0.8, v: -3.2 }],
|
||||
depths: [1.2, 1.2, 1.5, 1.8, 1.8, 1.5],
|
||||
coping: 1, tile: 1,
|
||||
},
|
||||
carve: true,
|
||||
inside(e, x, z) {
|
||||
const [u, v] = local(e, x, z);
|
||||
return pointInPoly(u, v, e.pts);
|
||||
},
|
||||
height(e, x, z) {
|
||||
const [u, v] = local(e, x, z);
|
||||
const dB = distToPoly(u, v, e.pts);
|
||||
// smooth floor: inverse-distance-weighted vertex depths (shallow end slopes to deep)
|
||||
let sw = 0, swd = 0;
|
||||
for (let i = 0; i < e.pts.length; i++) {
|
||||
const du = u - e.pts[i].u, dv = v - e.pts[i].v;
|
||||
const w = 1 / (du * du + dv * dv + 0.25);
|
||||
sw += w; swd += w * (e.depths[i] ?? 1.5);
|
||||
}
|
||||
const D = swd / sw;
|
||||
return -D + tranny(Math.min(dB, D), D);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function pointInPoly(u, v, pts) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
||||
const a = pts[i], b = pts[j];
|
||||
if ((a.v > v) !== (b.v > v) &&
|
||||
u < (b.u - a.u) * (v - a.v) / (b.v - a.v) + a.u) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
function distToPoly(u, v, pts) {
|
||||
let best = Infinity;
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const a = pts[i], b = pts[(i + 1) % pts.length];
|
||||
const du = b.u - a.u, dv = b.v - a.v;
|
||||
const t = clamp(((u - a.u) * du + (v - a.v) * dv) / (du * du + dv * dv || 1), 0, 1);
|
||||
best = Math.min(best, Math.hypot(u - (a.u + t * du), v - (a.v + t * dv)));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
// world-space rim polygon (editor handles, coping tube, tile bake)
|
||||
export function bowlRimWorld(e) {
|
||||
const c = Math.cos(e.rot || 0), s = Math.sin(e.rot || 0);
|
||||
return e.pts.map(p => [e.x + p.u * c - p.v * s, e.z + p.u * s + p.v * c]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- rails
|
||||
// v2 rails are polylines: { id, name, kind:'rail'|'ledge', profile:'round'|'square',
|
||||
// pts:[{x,z,y},...] }. v1 {a,b,ya,yb} files normalize on sight. The GAME contract
|
||||
@ -212,6 +266,15 @@ export function flattenRails(park) {
|
||||
out.push({ name: r.name, a: [a.x, a.z], b: [b.x, b.z], ya: a.y, yb: b.y, kind: r.kind });
|
||||
}
|
||||
}
|
||||
// pool coping rims are grindable steel: one ledge segment per rim edge
|
||||
for (const e of park.elements || []) {
|
||||
if (e.kind !== 'bowlpoly' || !e.coping) continue;
|
||||
const rim = bowlRimWorld(e);
|
||||
for (let i = 0; i < rim.length; i++) {
|
||||
const a = rim[i], b = rim[(i + 1) % rim.length];
|
||||
out.push({ name: 'Pool Coping', a, b, ya: 0.02, yb: 0.02, kind: 'ledge' });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -290,6 +353,8 @@ export function elementHit(e, x, z) {
|
||||
return u * u + v * v < 1; }
|
||||
case 'bowl': { const [u, v] = local(e, x, z);
|
||||
return Math.abs(u) <= e.size + M && Math.abs(v) <= e.size + M; }
|
||||
case 'bowlpoly': { const [u, v] = local(e, x, z);
|
||||
return pointInPoly(u, v, e.pts) || distToPoly(u, v, e.pts) <= M; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -325,6 +390,7 @@ export function elementOutline(e) {
|
||||
pts.push(W(Math.cos(a) * e.rx, Math.sin(a) * e.rz)); }
|
||||
return pts; }
|
||||
case 'bowl': return rect(-e.size, e.size, -e.size, e.size);
|
||||
case 'bowlpoly': return bowlRimWorld(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
32
js/ui.js
32
js/ui.js
@ -51,7 +51,8 @@ const PARAMS = {
|
||||
const PALETTE_GROUPS = [
|
||||
['TRANSITION', [['quarter', 'QUARTER'], ['corner', 'HIP/CORNER'], ['bank', 'BANK'],
|
||||
['kicker', 'KICKER'], ['spine', 'SPINE'], ['roller', 'ROLLER'],
|
||||
['mogul', 'MOGUL'], ['volcano', 'VOLCANO'], ['bowl', 'BOWL']]],
|
||||
['mogul', 'MOGUL'], ['volcano', 'VOLCANO'], ['bowl', 'BOWL'],
|
||||
['bowlpoly', 'CUSTOM BOWL']]],
|
||||
['STREET', [['funbox', 'FUNBOX'], ['pyramid', 'PYRAMID'], ['ledge', 'LEDGE'],
|
||||
['wedgepad', 'WEDGE PAD'], ['stairs', 'STAIRS'], ['dome', 'ISLAND']]],
|
||||
];
|
||||
@ -215,6 +216,35 @@ export function initUI(ed) {
|
||||
(v, done) => ed.updateSelected('rot', +(v * DEG).toFixed(4), { done })));
|
||||
for (const [key, label, min, max, step] of PARAMS[obj.kind] || [])
|
||||
inspector.append(numRow(obj, key, label, min, max, step, upd(key)));
|
||||
if (obj.kind === 'bowlpoly') {
|
||||
inspector.append(el('div', 'ihint',
|
||||
'drag the rim balls to shape it — inside corners, hips, whatever'));
|
||||
obj.depths.forEach((d, i) => {
|
||||
inspector.append(numRow({ d }, 'd', 'depth pt ' + (i + 1), 0.4, 3.4, 0.05,
|
||||
(v, done) => { obj.depths[i] = v; ed.updateSelected('depths', obj.depths, { done }); }));
|
||||
});
|
||||
const vbtns = el('div', 'ibtns');
|
||||
const addV = el('button', 'pbtn', '+ VERTEX');
|
||||
addV.onclick = () => {
|
||||
const a = obj.pts[obj.pts.length - 1], b = obj.pts[0];
|
||||
obj.pts.push({ u: +((a.u + b.u) / 2).toFixed(2), v: +((a.v + b.v) / 2).toFixed(2) });
|
||||
obj.depths.push(obj.depths[obj.depths.length - 1]);
|
||||
ed.updateSelected('pts', obj.pts, { done: true });
|
||||
};
|
||||
const delV = el('button', 'pbtn', '− VERTEX');
|
||||
delV.onclick = () => {
|
||||
if (obj.pts.length <= 3) return;
|
||||
obj.pts.pop(); obj.depths.pop();
|
||||
ed.updateSelected('pts', obj.pts, { done: true });
|
||||
};
|
||||
vbtns.append(addV, delV); inspector.append(vbtns);
|
||||
for (const [key, label] of [['coping', 'pool coping (grindable)'], ['tile', 'tile band']]) {
|
||||
const cb = el('label', 'snaprow');
|
||||
const inp = el('input'); inp.type = 'checkbox'; inp.checked = !!obj[key];
|
||||
inp.onchange = () => ed.updateSelected(key, inp.checked ? 1 : 0, { done: true });
|
||||
cb.append(inp, ' ' + label); inspector.append(cb);
|
||||
}
|
||||
}
|
||||
if (obj.kind === 'bowl') {
|
||||
const shape = el('select');
|
||||
shape.innerHTML = '<option value="0">square pool</option><option value="1">round bowl</option>';
|
||||
|
||||
215
parks/pool_demo.json
Normal file
215
parks/pool_demo.json
Normal file
@ -0,0 +1,215 @@
|
||||
{
|
||||
"name": "THE POOLROOM",
|
||||
"bounds": {
|
||||
"x0": -24,
|
||||
"x1": 24,
|
||||
"z0": -16,
|
||||
"z1": 16
|
||||
},
|
||||
"slabColor": "#8f958f",
|
||||
"grassColor": "#4d7c3c",
|
||||
"spawn": {
|
||||
"x": -18,
|
||||
"z": 10,
|
||||
"heading": 1.1
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"id": "e2_bqff",
|
||||
"kind": "bowlpoly",
|
||||
"x": -8,
|
||||
"z": 0,
|
||||
"rot": 0,
|
||||
"pts": [
|
||||
{
|
||||
"u": 5,
|
||||
"v": 0
|
||||
},
|
||||
{
|
||||
"u": 3.4,
|
||||
"v": 4
|
||||
},
|
||||
{
|
||||
"u": -2,
|
||||
"v": 4.6
|
||||
},
|
||||
{
|
||||
"u": -5.4,
|
||||
"v": 1.2
|
||||
},
|
||||
{
|
||||
"u": -4.4,
|
||||
"v": -3.4
|
||||
},
|
||||
{
|
||||
"u": 1.2,
|
||||
"v": -4.6
|
||||
}
|
||||
],
|
||||
"depths": [
|
||||
0.9,
|
||||
0.9,
|
||||
1.4,
|
||||
2.2,
|
||||
2.2,
|
||||
1.4
|
||||
],
|
||||
"coping": 1,
|
||||
"tile": 1
|
||||
},
|
||||
{
|
||||
"id": "e3_iu5u",
|
||||
"kind": "quarter",
|
||||
"x": 12,
|
||||
"z": -16,
|
||||
"rot": 0,
|
||||
"len": 20,
|
||||
"r0": 1.8,
|
||||
"r1": 1.8,
|
||||
"lump": 0,
|
||||
"deck": 0.6,
|
||||
"vert": 0
|
||||
},
|
||||
{
|
||||
"id": "e4_fhid",
|
||||
"kind": "kicker",
|
||||
"x": 10,
|
||||
"z": 6,
|
||||
"rot": 3.141592653589793,
|
||||
"len": 2.4,
|
||||
"h": 0.7,
|
||||
"run": 1.8,
|
||||
"curve": 0.6,
|
||||
"deck": 0.25
|
||||
},
|
||||
{
|
||||
"id": "e5_g65t",
|
||||
"kind": "roller",
|
||||
"x": 16,
|
||||
"z": 0,
|
||||
"rot": 1.5708,
|
||||
"len": 8,
|
||||
"h": 0.45,
|
||||
"s": 1.1
|
||||
},
|
||||
{
|
||||
"id": "e6_dthi",
|
||||
"kind": "ledge",
|
||||
"x": 6,
|
||||
"z": 12,
|
||||
"rot": 0,
|
||||
"hw": 1.5,
|
||||
"hd": 0.8,
|
||||
"h": 0.22
|
||||
}
|
||||
],
|
||||
"rails": [
|
||||
{
|
||||
"id": "rb",
|
||||
"name": "Rainbow Rail",
|
||||
"kind": "rail",
|
||||
"profile": "round",
|
||||
"pts": [
|
||||
{
|
||||
"x": 2,
|
||||
"z": -10,
|
||||
"y": 0.25
|
||||
},
|
||||
{
|
||||
"x": 2.667,
|
||||
"z": -10,
|
||||
"y": 0.7749999999999999
|
||||
},
|
||||
{
|
||||
"x": 3.334,
|
||||
"z": -10,
|
||||
"y": 1.1593266739736605
|
||||
},
|
||||
{
|
||||
"x": 4.001,
|
||||
"z": -10,
|
||||
"y": 1.3
|
||||
},
|
||||
{
|
||||
"x": 4.668,
|
||||
"z": -10,
|
||||
"y": 1.1593266739736605
|
||||
},
|
||||
{
|
||||
"x": 5.335,
|
||||
"z": -10,
|
||||
"y": 0.7749999999999999
|
||||
},
|
||||
{
|
||||
"x": 6.002000000000001,
|
||||
"z": -10,
|
||||
"y": 0.2500000000000001
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sq",
|
||||
"name": "Square Bar",
|
||||
"kind": "ledge",
|
||||
"profile": "square",
|
||||
"pts": [
|
||||
{
|
||||
"x": -16,
|
||||
"z": -6,
|
||||
"y": 0.4
|
||||
},
|
||||
{
|
||||
"x": -13,
|
||||
"z": -6,
|
||||
"y": 0.4
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"gaps": [
|
||||
{
|
||||
"id": "g1",
|
||||
"name": "OVER THE DEEP END",
|
||||
"x": -11,
|
||||
"z": 0,
|
||||
"r": 2.4
|
||||
}
|
||||
],
|
||||
"letters": [],
|
||||
"zones": [],
|
||||
"decals": [],
|
||||
"props": [
|
||||
{
|
||||
"id": "w1",
|
||||
"src": "builtin:picnic",
|
||||
"x": -18,
|
||||
"z": -12,
|
||||
"rot": 0.4,
|
||||
"scale": 1
|
||||
},
|
||||
{
|
||||
"id": "w2",
|
||||
"src": "builtin:floodlight",
|
||||
"x": 20,
|
||||
"z": 12,
|
||||
"rot": 0,
|
||||
"scale": 1
|
||||
},
|
||||
{
|
||||
"id": "w3",
|
||||
"src": "builtin:fence",
|
||||
"x": 0,
|
||||
"z": -15.5,
|
||||
"rot": 0,
|
||||
"scale": 1
|
||||
},
|
||||
{
|
||||
"id": "w4",
|
||||
"src": "builtin:bleachers",
|
||||
"x": -20,
|
||||
"z": 4,
|
||||
"rot": 1.5708,
|
||||
"scale": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user