spoint 0.1.667 → 0.1.669

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ // Boot-diagnostics helpers for app.js: shadow-cascade-count resolution (device-aware default the
2
+ // shadow pipeline registry itself has no knowledge of) and the single boot-failure overlay renderer,
3
+ // reused by every early-boot catch site (WebGL2-context-creation failure, window.onerror,
4
+ // unhandledrejection). Split out as the only two functions in app.js's top-level boot script with no
5
+ // interleaved side-effecting script logic immediately around them -- app.js itself is a flat top-to-
6
+ // bottom boot sequence, not a factory, so most of it cannot be safely cut without risking a module-eval
7
+ // ordering regression; these two are genuinely self-contained.
8
+
9
+ import { QualityPresets } from './core/QualityPresets.js'
10
+
11
+ function _shadowCascadeCountForBoot(deviceInfo) {
12
+ const explicit = typeof window !== 'undefined' ? window.__shadowCascades : undefined
13
+ if (Number.isFinite(explicit)) return Math.max(1, Math.min(3, Math.round(explicit)))
14
+ const presetName = QualityPresets.getPersisted() || QualityPresets.chooseInitialPreset(deviceInfo)
15
+ const byTier = { Low: 1, Medium: 1, High: 2, Ultra: 3 }
16
+ return byTier[presetName] || 1
17
+ }
18
+
19
+ // Single boot-failure overlay renderer, reused by both the WebGL2-specific catch below AND the
20
+ // generic window.onerror/unhandledrejection listeners -- one visual style for "the client failed
21
+ // to boot" regardless of WHERE in the boot sequence it failed, instead of a blank canvas / silently
22
+ // stuck loading screen. Idempotent (a data-flag guards double-render across multiple errors racing
23
+ // in, e.g. a synchronous throw immediately followed by a rejected promise from the same root cause).
24
+ let _bootFailureShown = false
25
+ function _showBootFailureOverlay(title, detail) {
26
+ if (_bootFailureShown) return
27
+ _bootFailureShown = true
28
+ try {
29
+ const o = document.createElement('div')
30
+ o.setAttribute('role', 'alert')
31
+ o.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:24px;background:#0b0e14;color:#e6e6e6;font:16px/1.5 system-ui,sans-serif;text-align:center'
32
+ const titleEl = document.createElement('div')
33
+ titleEl.style.cssText = 'font-size:22px;font-weight:600;margin-bottom:12px'
34
+ titleEl.textContent = title
35
+ const detailEl = document.createElement('div')
36
+ detailEl.style.cssText = 'max-width:640px;white-space:pre-wrap;word-break:break-word;font:13px/1.5 ui-monospace,monospace;opacity:0.85;margin-top:8px;text-align:left'
37
+ detailEl.textContent = detail || ''
38
+ const wrap = document.createElement('div')
39
+ wrap.style.cssText = 'max-width:640px'
40
+ wrap.appendChild(titleEl)
41
+ wrap.appendChild(detailEl)
42
+ o.appendChild(wrap)
43
+ document.body.appendChild(o)
44
+ const ls = document.getElementById('loading-screen') || document.querySelector('.loading-screen')
45
+ if (ls) ls.style.display = 'none'
46
+ } catch (_) { /* overlay itself must never throw during an already-failing boot */ }
47
+ }
48
+
49
+ export { _shadowCascadeCountForBoot, _showBootFailureOverlay }
package/client/app.js CHANGED
@@ -62,6 +62,7 @@ import { getSharedStreamingScheduler } from './core/StreamingScheduler.js'
62
62
  import { createPlacementScheduler } from './core/PlacementScheduler.js'
63
63
  import { getSharedCacheRevalidationSweep } from './core/CacheRevalidationSweep.js'
64
64
  import { QualityPresets, installQualityPresets } from './core/QualityPresets.js'
65
+ import { _shadowCascadeCountForBoot, _showBootFailureOverlay } from './BootFailureOverlay.js'
65
66
  import { createSettingsMenu } from './hud/SettingsMenu.js'
66
67
  import { createPauseMenu } from './hud/PauseMenu.js'
67
68
  import { createEmoteWheel } from './hud/EmoteWheel.js'
@@ -118,48 +119,6 @@ if (typeof window !== 'undefined') window.__deviceInfo = _deviceInfoEarly
118
119
  // Device-tier default for ShadowPipeline's cascade count (shadowCascades RenderControls knob, doc'd
119
120
  // in RenderControls.js: Low/Medium=1, High=2, Ultra=3). RenderControls.get() itself only ever returns
120
121
  // an explicit window.__shadowCascades override or the registry's static default (1) -- it has no
121
- // device-awareness of its own -- so a real tier default has to be resolved here, once, the same way
122
- // QualityPresets.autoApplyPersisted resolves a tier below (persisted user choice wins, else the same
123
- // chooseInitialPreset heuristic), and passed into createShadowPipeline explicitly. window.__shadowCascades
124
- // (if pre-set before this script runs, e.g. by a test harness) always wins over both.
125
- function _shadowCascadeCountForBoot(deviceInfo) {
126
- const explicit = typeof window !== 'undefined' ? window.__shadowCascades : undefined
127
- if (Number.isFinite(explicit)) return Math.max(1, Math.min(3, Math.round(explicit)))
128
- const presetName = QualityPresets.getPersisted() || QualityPresets.chooseInitialPreset(deviceInfo)
129
- const byTier = { Low: 1, Medium: 1, High: 2, Ultra: 3 }
130
- return byTier[presetName] || 1
131
- }
132
-
133
- // Single boot-failure overlay renderer, reused by both the WebGL2-specific catch below AND the
134
- // generic window.onerror/unhandledrejection listeners -- one visual style for "the client failed
135
- // to boot" regardless of WHERE in the boot sequence it failed, instead of a blank canvas / silently
136
- // stuck loading screen. Idempotent (a data-flag guards double-render across multiple errors racing
137
- // in, e.g. a synchronous throw immediately followed by a rejected promise from the same root cause).
138
- let _bootFailureShown = false
139
- function _showBootFailureOverlay(title, detail) {
140
- if (_bootFailureShown) return
141
- _bootFailureShown = true
142
- try {
143
- const o = document.createElement('div')
144
- o.setAttribute('role', 'alert')
145
- o.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:24px;background:#0b0e14;color:#e6e6e6;font:16px/1.5 system-ui,sans-serif;text-align:center'
146
- const titleEl = document.createElement('div')
147
- titleEl.style.cssText = 'font-size:22px;font-weight:600;margin-bottom:12px'
148
- titleEl.textContent = title
149
- const detailEl = document.createElement('div')
150
- detailEl.style.cssText = 'max-width:640px;white-space:pre-wrap;word-break:break-word;font:13px/1.5 ui-monospace,monospace;opacity:0.85;margin-top:8px;text-align:left'
151
- detailEl.textContent = detail || ''
152
- const wrap = document.createElement('div')
153
- wrap.style.cssText = 'max-width:640px'
154
- wrap.appendChild(titleEl)
155
- wrap.appendChild(detailEl)
156
- o.appendChild(wrap)
157
- document.body.appendChild(o)
158
- const ls = document.getElementById('loading-screen') || document.querySelector('.loading-screen')
159
- if (ls) ls.style.display = 'none'
160
- } catch (_) { /* overlay itself must never throw during an already-failing boot */ }
161
- }
162
-
163
122
  // Generic boot-failure safety net: ANY uncaught error or unhandled promise rejection during client
164
123
  // boot (not just the WebGL2-context-creation case caught explicitly below) renders the same overlay
165
124
  // style with the real error message/stack, instead of leaving the user staring at a stuck loading
@@ -0,0 +1,150 @@
1
+ // Gizmo mesh-building helpers for editor.js's createEditor: translate/rotate/scale handle geometry,
2
+ // hit-proxy construction, the closest-point-on-axis-line drag math, and the trigger-volume radius
3
+ // gizmo. Split out as editor.js's largest self-contained block -- each function here only touches
4
+ // THREE + its own params/module constants, never createEditor's own closure state (gizmoGroup,
5
+ // machine, scene, radiusGizmoGroup all stay in editor.js since attachGizmo/attachRadiusGizmo/
6
+ // _highlightAxis mutate them directly).
7
+
8
+ import * as THREE from 'three'
9
+
10
+ const _coarsePointer = () => (typeof matchMedia === 'function' && matchMedia('(pointer:coarse)').matches)
11
+
12
+ const RADIUS_HANDLE_COUNT = 4 // 4 draggable ring handles (N/E/S/W) for an easier target than the thin ring itself
13
+ const _RADIUS_COLOR = 0xffaa00
14
+
15
+ // Closest point on the 3D LINE through `origin` along direction `axis` to the mouse `ray` -- the
16
+ // standard closed-form for two skew lines' closest approach, well-conditioned for any camera
17
+ // angle except the ray running exactly parallel to axis (see onStart's comment for why this
18
+ // replaces plane-intersection for every single-axis dot-product-consumed drag response).
19
+ function _closestPointOnAxisLine(ray, origin, axis) {
20
+ // Line 1: ray.origin + t1*ray.direction (mouse ray). Line 2: origin + t2*axis (the handle's axis).
21
+ const w0 = ray.origin.clone().sub(origin)
22
+ const a = ray.direction.dot(ray.direction) // == 1 (ray.direction is unit length)
23
+ const b = ray.direction.dot(axis)
24
+ const c = axis.dot(axis) // == 1 (_axisVec always returns unit length)
25
+ const d = ray.direction.dot(w0)
26
+ const e = axis.dot(w0)
27
+ const denom = a * c - b * b
28
+ // denom -> 0 only when the ray runs parallel to axis (camera looking straight down the handle);
29
+ // fall back to the origin itself rather than dividing by ~0 into a huge/NaN point.
30
+ const t2 = Math.abs(denom) < 1e-8 ? 0 : (a * e - b * d) / denom
31
+ return origin.clone().addScaledVector(axis, t2)
32
+ }
33
+
34
+ // Invisible enlarged pick proxy: coarse pointers get a bigger raycast target than the 0.04-thick visible handle.
35
+ function _addHitProxy(group, axis, geom, place) {
36
+ const proxy = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ visible: false }))
37
+ proxy.visible = false
38
+ proxy.userData.gizmoAxis = axis
39
+ proxy.userData.isHitProxy = true
40
+ proxy.renderOrder = 1000
41
+ place(proxy)
42
+ group.add(proxy)
43
+ }
44
+ function _axisHitProxy(group, axis) {
45
+ const fat = _coarsePointer() ? 0.34 : 0.14
46
+ const geom = new THREE.CylinderGeometry(fat, fat, 1.3, 6)
47
+ geom.translate(0, 0.65, 0)
48
+ _addHitProxy(group, axis, geom, (p) => {
49
+ if (axis === 'x') p.rotation.z = -Math.PI / 2
50
+ else if (axis === 'z') p.rotation.x = Math.PI / 2
51
+ })
52
+ }
53
+ function _ringHitProxy(group, axis, rx, ry) {
54
+ const fat = _coarsePointer() ? 0.2 : 0.08
55
+ const geom = new THREE.TorusGeometry(1, fat, 6, 24)
56
+ _addHitProxy(group, axis, geom, (p) => { p.rotation.x = rx; p.rotation.y = ry })
57
+ }
58
+ const _HIGHLIGHT = 0xffff00
59
+ function _tagBaseColor(mesh) { mesh.userData.baseColor = mesh.material.color.getHex(); return mesh }
60
+ function buildTranslateGizmo() {
61
+ const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'translate'
62
+ for (const [axis, color, rx, rz] of [['x',0xff2222,0,-Math.PI/2],['y',0x22ff22,0,0],['z',0x2222ff,Math.PI/2,0]]) {
63
+ const mat = new THREE.MeshBasicMaterial({ color, depthTest: false })
64
+ const shaft = _tagBaseColor(new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1, 8), mat))
65
+ shaft.geometry.translate(0, 0.5, 0); shaft.rotation.x = rx; shaft.rotation.z = rz
66
+ shaft.userData.gizmoAxis = axis; shaft.renderOrder = 999
67
+ const cap = _tagBaseColor(new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.25, 8), mat))
68
+ cap.geometry.translate(0, 0.125, 0)
69
+ if (axis === 'x') { cap.rotation.z = -Math.PI/2; cap.position.set(1, 0, 0) }
70
+ else if (axis === 'y') cap.position.set(0, 1, 0)
71
+ else { cap.rotation.x = Math.PI/2; cap.position.set(0, 0, 1) }
72
+ cap.userData.gizmoAxis = axis; cap.renderOrder = 999
73
+ g.add(shaft); g.add(cap)
74
+ _axisHitProxy(g, axis)
75
+ }
76
+ return g
77
+ }
78
+ function buildRotateGizmo() {
79
+ const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'rotate'
80
+ for (const [axis,color,rx,ry] of [['x',0xff2222,0,Math.PI/2],['y',0x22ff22,Math.PI/2,0],['z',0x2222ff,0,0]]) {
81
+ const ring = _tagBaseColor(new THREE.Mesh(new THREE.TorusGeometry(1,0.04,8,32),new THREE.MeshBasicMaterial({color,depthTest:false,side:THREE.DoubleSide})))
82
+ ring.rotation.x=rx;ring.rotation.y=ry;ring.userData.gizmoAxis=axis;ring.renderOrder=999;g.add(ring)
83
+ _ringHitProxy(g, axis, rx, ry)
84
+ }
85
+ return g
86
+ }
87
+ function buildScaleGizmo() {
88
+ const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'scale'
89
+ for (const [axis,color,rx,rz,px,py,pz] of [['x',0xff2222,0,-Math.PI/2,1,0,0],['y',0x22ff22,0,0,0,1,0],['z',0x2222ff,Math.PI/2,0,0,0,1]]) {
90
+ const mat=new THREE.MeshBasicMaterial({color,depthTest:false})
91
+ const shaft=_tagBaseColor(new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.04,1,8),mat));shaft.geometry.translate(0,0.5,0);shaft.rotation.x=rx;shaft.rotation.z=rz;shaft.userData.gizmoAxis=axis;shaft.renderOrder=999
92
+ const box=_tagBaseColor(new THREE.Mesh(new THREE.BoxGeometry(0.2,0.2,0.2),mat));box.position.set(px,py,pz);box.userData.gizmoAxis=axis;box.renderOrder=999
93
+ g.add(shaft);g.add(box)
94
+ _axisHitProxy(g, axis)
95
+ }
96
+ return g
97
+ }
98
+ function _highlightAxis(axis) {
99
+ if (!gizmoGroup) return
100
+ gizmoGroup.children.forEach(c => {
101
+ if (!c.userData.gizmoAxis || c.userData.isHitProxy || c.userData.baseColor === undefined) return
102
+ c.material.color.setHex(c.userData.gizmoAxis === axis ? _HIGHLIGHT : c.userData.baseColor)
103
+ })
104
+ }
105
+ function _buildGizmo() { return _mode()==='rotate'?buildRotateGizmo():_mode()==='scale'?buildScaleGizmo():buildTranslateGizmo() }
106
+
107
+ // Radius-drag handle (trigger-volume-radius-gizmo-handle): a flat horizontal ring at the entity's
108
+ // Y position, radius matching custom.radius, shown ADDITIONALLY alongside whichever translate/
109
+ // rotate/scale gizmo is currently active -- not a 4th gizmoMode, since a radius-shaped entity still
110
+ // wants normal move/rotate/scale on its position/transform too. Separate group (radiusGizmoGroup)
111
+ // so it survives independently of gizmoGroup's per-mode rebuild in _buildGizmo/attachGizmo.
112
+ function _entityHasRadiusGizmo(mesh) {
113
+ // Scoped to trigger-volume-shaped entities (custom._trigger, set by apps/trigger-volume/index.js
114
+ // setup()) for this slice -- capture-zone/shrinking-zone-center reuse is a separate PRD row
115
+ // (capture-zone-shrinking-zone-gizmo-followup-check) pending a check of whether their radius
116
+ // semantics (capture-zone: static; shrinking-zone-center: server-shrinks-over-time) allow the
117
+ // identical handle without a live-vs-authored-value conflict.
118
+ return !!(mesh && mesh.userData?.custom?._trigger)
119
+ }
120
+ function _entityRadius(mesh) {
121
+ const r = mesh?.userData?.custom?.radius
122
+ return (typeof r === 'number' && Number.isFinite(r) && r > 0) ? r : 3
123
+ }
124
+ function buildRadiusGizmo(radius) {
125
+ const g = new THREE.Group(); g.userData.isRadiusGizmo = true
126
+ const ringMat = new THREE.MeshBasicMaterial({ color: _RADIUS_COLOR, depthTest: false, transparent: true, opacity: 0.85 })
127
+ const ring = _tagBaseColor(new THREE.Mesh(new THREE.TorusGeometry(radius, 0.03, 6, 48), ringMat))
128
+ ring.rotation.x = Math.PI / 2 // lie flat in the XZ plane (horizontal, matching a ground-footprint radius)
129
+ ring.userData.gizmoAxis = 'radius'
130
+ ring.renderOrder = 999
131
+ g.add(ring)
132
+ // 4 cardinal handle knobs, each an enlarged hit target (coarse-pointer-aware like _axisHitProxy)
133
+ // sitting ON the ring so a drag can start from any of 4 directions, not just a thin-torus pick.
134
+ const fat = _coarsePointer() ? 0.22 : 0.1
135
+ for (let i = 0; i < RADIUS_HANDLE_COUNT; i++) {
136
+ const ang = (i / RADIUS_HANDLE_COUNT) * Math.PI * 2
137
+ const knob = _tagBaseColor(new THREE.Mesh(new THREE.SphereGeometry(fat, 10, 8), new THREE.MeshBasicMaterial({ color: _RADIUS_COLOR, depthTest: false, transparent: true, opacity: 0.85 })))
138
+ knob.position.set(Math.cos(ang) * radius, 0, Math.sin(ang) * radius)
139
+ knob.userData.gizmoAxis = 'radius'
140
+ knob.renderOrder = 1000
141
+ g.add(knob)
142
+ }
143
+ return g
144
+ }
145
+
146
+ export {
147
+ _closestPointOnAxisLine, _addHitProxy, _axisHitProxy, _ringHitProxy, _tagBaseColor,
148
+ buildTranslateGizmo, buildRotateGizmo, buildScaleGizmo,
149
+ _entityHasRadiusGizmo, _entityRadius, buildRadiusGizmo, RADIUS_HANDLE_COUNT
150
+ }
@@ -4,8 +4,11 @@ import { MSG } from '/src/protocol/MessageTypes.js'
4
4
  import { showToast } from './EditPanelDOM.js'
5
5
  import { STRINGS } from '../core/strings.js'
6
6
  import { createWaypointPathOverlay } from './WaypointPath.js'
7
-
8
- const _coarsePointer = () => (typeof matchMedia === 'function' && matchMedia('(pointer:coarse)').matches)
7
+ import {
8
+ _closestPointOnAxisLine, _addHitProxy, _axisHitProxy, _ringHitProxy, _tagBaseColor,
9
+ buildTranslateGizmo, buildRotateGizmo, buildScaleGizmo,
10
+ _entityHasRadiusGizmo, _entityRadius, buildRadiusGizmo, RADIUS_HANDLE_COUNT
11
+ } from './EditorGizmoBuild.js'
9
12
 
10
13
  // --- Camera bookmarks (editor-camera-bookmarks) -------------------------------------------
11
14
  // localStorage-persisted per-world (keyed by the ?world= URL param, falling back to 'default'
@@ -76,90 +79,8 @@ export function createEditor({ scene, camera, renderer, client, entityMeshes, pl
76
79
  // intersection -- applyGizmoDrag's per-move-frame sampling must reuse the SAME technique the
77
80
  // drag started with, or the delta comparison mixes two different coordinate derivations.
78
81
  let _dragUsesAxisLine = false
79
-
80
- // Closest point on the 3D LINE through `origin` along direction `axis` to the mouse `ray` -- the
81
- // standard closed-form for two skew lines' closest approach, well-conditioned for any camera
82
- // angle except the ray running exactly parallel to axis (see onStart's comment for why this
83
- // replaces plane-intersection for every single-axis dot-product-consumed drag response).
84
- function _closestPointOnAxisLine(ray, origin, axis) {
85
- // Line 1: ray.origin + t1*ray.direction (mouse ray). Line 2: origin + t2*axis (the handle's axis).
86
- const w0 = ray.origin.clone().sub(origin)
87
- const a = ray.direction.dot(ray.direction) // == 1 (ray.direction is unit length)
88
- const b = ray.direction.dot(axis)
89
- const c = axis.dot(axis) // == 1 (_axisVec always returns unit length)
90
- const d = ray.direction.dot(w0)
91
- const e = axis.dot(w0)
92
- const denom = a * c - b * b
93
- // denom -> 0 only when the ray runs parallel to axis (camera looking straight down the handle);
94
- // fall back to the origin itself rather than dividing by ~0 into a huge/NaN point.
95
- const t2 = Math.abs(denom) < 1e-8 ? 0 : (a * e - b * d) / denom
96
- return origin.clone().addScaledVector(axis, t2)
97
- }
98
-
99
- // Invisible enlarged pick proxy: coarse pointers get a bigger raycast target than the 0.04-thick visible handle.
100
- function _addHitProxy(group, axis, geom, place) {
101
- const proxy = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ visible: false }))
102
- proxy.visible = false
103
- proxy.userData.gizmoAxis = axis
104
- proxy.userData.isHitProxy = true
105
- proxy.renderOrder = 1000
106
- place(proxy)
107
- group.add(proxy)
108
- }
109
- function _axisHitProxy(group, axis) {
110
- const fat = _coarsePointer() ? 0.34 : 0.14
111
- const geom = new THREE.CylinderGeometry(fat, fat, 1.3, 6)
112
- geom.translate(0, 0.65, 0)
113
- _addHitProxy(group, axis, geom, (p) => {
114
- if (axis === 'x') p.rotation.z = -Math.PI / 2
115
- else if (axis === 'z') p.rotation.x = Math.PI / 2
116
- })
117
- }
118
- function _ringHitProxy(group, axis, rx, ry) {
119
- const fat = _coarsePointer() ? 0.2 : 0.08
120
- const geom = new THREE.TorusGeometry(1, fat, 6, 24)
121
- _addHitProxy(group, axis, geom, (p) => { p.rotation.x = rx; p.rotation.y = ry })
122
- }
123
82
  const _HIGHLIGHT = 0xffff00
124
- function _tagBaseColor(mesh) { mesh.userData.baseColor = mesh.material.color.getHex(); return mesh }
125
- function buildTranslateGizmo() {
126
- const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'translate'
127
- for (const [axis, color, rx, rz] of [['x',0xff2222,0,-Math.PI/2],['y',0x22ff22,0,0],['z',0x2222ff,Math.PI/2,0]]) {
128
- const mat = new THREE.MeshBasicMaterial({ color, depthTest: false })
129
- const shaft = _tagBaseColor(new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1, 8), mat))
130
- shaft.geometry.translate(0, 0.5, 0); shaft.rotation.x = rx; shaft.rotation.z = rz
131
- shaft.userData.gizmoAxis = axis; shaft.renderOrder = 999
132
- const cap = _tagBaseColor(new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.25, 8), mat))
133
- cap.geometry.translate(0, 0.125, 0)
134
- if (axis === 'x') { cap.rotation.z = -Math.PI/2; cap.position.set(1, 0, 0) }
135
- else if (axis === 'y') cap.position.set(0, 1, 0)
136
- else { cap.rotation.x = Math.PI/2; cap.position.set(0, 0, 1) }
137
- cap.userData.gizmoAxis = axis; cap.renderOrder = 999
138
- g.add(shaft); g.add(cap)
139
- _axisHitProxy(g, axis)
140
- }
141
- return g
142
- }
143
- function buildRotateGizmo() {
144
- const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'rotate'
145
- for (const [axis,color,rx,ry] of [['x',0xff2222,0,Math.PI/2],['y',0x22ff22,Math.PI/2,0],['z',0x2222ff,0,0]]) {
146
- const ring = _tagBaseColor(new THREE.Mesh(new THREE.TorusGeometry(1,0.04,8,32),new THREE.MeshBasicMaterial({color,depthTest:false,side:THREE.DoubleSide})))
147
- ring.rotation.x=rx;ring.rotation.y=ry;ring.userData.gizmoAxis=axis;ring.renderOrder=999;g.add(ring)
148
- _ringHitProxy(g, axis, rx, ry)
149
- }
150
- return g
151
- }
152
- function buildScaleGizmo() {
153
- const g = new THREE.Group(); g.userData.isGizmo = true; g.userData.mode = 'scale'
154
- for (const [axis,color,rx,rz,px,py,pz] of [['x',0xff2222,0,-Math.PI/2,1,0,0],['y',0x22ff22,0,0,0,1,0],['z',0x2222ff,Math.PI/2,0,0,0,1]]) {
155
- const mat=new THREE.MeshBasicMaterial({color,depthTest:false})
156
- const shaft=_tagBaseColor(new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.04,1,8),mat));shaft.geometry.translate(0,0.5,0);shaft.rotation.x=rx;shaft.rotation.z=rz;shaft.userData.gizmoAxis=axis;shaft.renderOrder=999
157
- const box=_tagBaseColor(new THREE.Mesh(new THREE.BoxGeometry(0.2,0.2,0.2),mat));box.position.set(px,py,pz);box.userData.gizmoAxis=axis;box.renderOrder=999
158
- g.add(shaft);g.add(box)
159
- _axisHitProxy(g, axis)
160
- }
161
- return g
162
- }
83
+
163
84
  function _highlightAxis(axis) {
164
85
  if (!gizmoGroup) return
165
86
  gizmoGroup.children.forEach(c => {
@@ -175,41 +96,7 @@ export function createEditor({ scene, camera, renderer, client, entityMeshes, pl
175
96
  // wants normal move/rotate/scale on its position/transform too. Separate group (radiusGizmoGroup)
176
97
  // so it survives independently of gizmoGroup's per-mode rebuild in _buildGizmo/attachGizmo.
177
98
  let radiusGizmoGroup = null
178
- const RADIUS_HANDLE_COUNT = 4 // 4 draggable ring handles (N/E/S/W) for an easier target than the thin ring itself
179
- const _RADIUS_COLOR = 0xffaa00, _RADIUS_HIGHLIGHT = 0xffff00
180
- function _entityHasRadiusGizmo(mesh) {
181
- // Scoped to trigger-volume-shaped entities (custom._trigger, set by apps/trigger-volume/index.js
182
- // setup()) for this slice -- capture-zone/shrinking-zone-center reuse is a separate PRD row
183
- // (capture-zone-shrinking-zone-gizmo-followup-check) pending a check of whether their radius
184
- // semantics (capture-zone: static; shrinking-zone-center: server-shrinks-over-time) allow the
185
- // identical handle without a live-vs-authored-value conflict.
186
- return !!(mesh && mesh.userData?.custom?._trigger)
187
- }
188
- function _entityRadius(mesh) {
189
- const r = mesh?.userData?.custom?.radius
190
- return (typeof r === 'number' && Number.isFinite(r) && r > 0) ? r : 3
191
- }
192
- function buildRadiusGizmo(radius) {
193
- const g = new THREE.Group(); g.userData.isRadiusGizmo = true
194
- const ringMat = new THREE.MeshBasicMaterial({ color: _RADIUS_COLOR, depthTest: false, transparent: true, opacity: 0.85 })
195
- const ring = _tagBaseColor(new THREE.Mesh(new THREE.TorusGeometry(radius, 0.03, 6, 48), ringMat))
196
- ring.rotation.x = Math.PI / 2 // lie flat in the XZ plane (horizontal, matching a ground-footprint radius)
197
- ring.userData.gizmoAxis = 'radius'
198
- ring.renderOrder = 999
199
- g.add(ring)
200
- // 4 cardinal handle knobs, each an enlarged hit target (coarse-pointer-aware like _axisHitProxy)
201
- // sitting ON the ring so a drag can start from any of 4 directions, not just a thin-torus pick.
202
- const fat = _coarsePointer() ? 0.22 : 0.1
203
- for (let i = 0; i < RADIUS_HANDLE_COUNT; i++) {
204
- const ang = (i / RADIUS_HANDLE_COUNT) * Math.PI * 2
205
- const knob = _tagBaseColor(new THREE.Mesh(new THREE.SphereGeometry(fat, 10, 8), new THREE.MeshBasicMaterial({ color: _RADIUS_COLOR, depthTest: false, transparent: true, opacity: 0.85 })))
206
- knob.position.set(Math.cos(ang) * radius, 0, Math.sin(ang) * radius)
207
- knob.userData.gizmoAxis = 'radius'
208
- knob.renderOrder = 1000
209
- g.add(knob)
210
- }
211
- return g
212
- }
99
+ const _RADIUS_HIGHLIGHT = 0xffff00
213
100
  function attachRadiusGizmo(mesh) {
214
101
  if (radiusGizmoGroup) { scene.remove(radiusGizmoGroup); radiusGizmoGroup = null }
215
102
  if (!_entityHasRadiusGizmo(mesh)) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.667",
3
+ "version": "0.1.669",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [