@operato/scene-storage 10.0.0-beta.28 → 10.0.0-beta.30
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.
- package/CHANGELOG.md +15 -0
- package/dist/asrs-crane-3d.d.ts +10 -0
- package/dist/asrs-crane-3d.js +17 -0
- package/dist/asrs-crane-3d.js.map +1 -1
- package/dist/asrs-crane.d.ts +49 -13
- package/dist/asrs-crane.js +120 -16
- package/dist/asrs-crane.js.map +1 -1
- package/dist/asrs-rack.d.ts +49 -19
- package/dist/asrs-rack.js +108 -20
- package/dist/asrs-rack.js.map +1 -1
- package/dist/box.d.ts +3 -3
- package/dist/box.js +1 -2
- package/dist/box.js.map +1 -1
- package/dist/generic-container.d.ts +2 -2
- package/dist/generic-container.js +1 -2
- package/dist/generic-container.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.d.ts +2 -2
- package/dist/pallet.js +1 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.d.ts +3 -3
- package/dist/parcel.js +1 -2
- package/dist/parcel.js.map +1 -1
- package/dist/rack-cell-3d.d.ts +25 -0
- package/dist/rack-cell-3d.js +88 -0
- package/dist/rack-cell-3d.js.map +1 -0
- package/dist/rack-cell.d.ts +56 -0
- package/dist/rack-cell.js +200 -0
- package/dist/rack-cell.js.map +1 -0
- package/dist/spot.d.ts +4 -11
- package/dist/spot.js +2 -3
- package/dist/spot.js.map +1 -1
- package/dist/templates/index.d.ts +42 -0
- package/dist/templates/index.js +43 -1
- package/dist/templates/index.js.map +1 -1
- package/package.json +9 -4
- package/src/asrs-crane-3d.ts +20 -0
- package/src/asrs-crane.ts +137 -16
- package/src/asrs-rack.ts +119 -20
- package/src/box.ts +2 -4
- package/src/generic-container.ts +1 -3
- package/src/index.ts +3 -0
- package/src/pallet.ts +1 -3
- package/src/parcel.ts +2 -4
- package/src/rack-cell-3d.ts +101 -0
- package/src/rack-cell.ts +228 -0
- package/src/spot.ts +4 -5
- package/src/templates/index.ts +43 -1
- package/test/setup.js +279 -0
- package/test/test-asrs-crane.ts +319 -0
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/rack-cell.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* RackCell — a single storage slot within an AsrsRack.
|
|
5
|
+
*
|
|
6
|
+
* A RackCell is a virtual component: it occupies a specific (bay, row, level)
|
|
7
|
+
* coordinate within the parent rack and acts as a CarrierHolder for one carrier
|
|
8
|
+
* (or several, depending on `cellType`).
|
|
9
|
+
*
|
|
10
|
+
* The crane (AsrsCrane) navigates toward a RackCell as its `place()` destination —
|
|
11
|
+
* because the rack cell is a discrete component, Mover.moveTo() can target it
|
|
12
|
+
* directly and arrive at exactly the right bay × level position.
|
|
13
|
+
*
|
|
14
|
+
* Visual: invisible in 2D (no visible 2D footprint — rack cells don't make
|
|
15
|
+
* sense as 2D top-down boxes). In 3D, each cell is an invisible Group
|
|
16
|
+
* positioned within the rack's coordinate space (RackCell3D handles
|
|
17
|
+
* this via updateTransform override).
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle: AsrsRack._buildCells() instantiates RackCell children.
|
|
20
|
+
* Do not create RackCell components manually — they are managed by the rack.
|
|
21
|
+
*
|
|
22
|
+
* Domain aliases:
|
|
23
|
+
* cell.store(carrier) ← cell.receive(carrier)
|
|
24
|
+
* cell.retrieve(carrier, target) ← cell.dispatch(carrier, target)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
Component,
|
|
29
|
+
ComponentNature,
|
|
30
|
+
ContainerAbstract,
|
|
31
|
+
RealObject,
|
|
32
|
+
TRANSFER_SLOT_KEY,
|
|
33
|
+
sceneComponent
|
|
34
|
+
} from '@hatiolab/things-scene'
|
|
35
|
+
import { CarrierHolder, type AttachFrame } from '@operato/scene-base'
|
|
36
|
+
|
|
37
|
+
import { RackCell3D } from './rack-cell-3d.js'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many carriers a cell can hold simultaneously.
|
|
41
|
+
* - single: exactly 1 (typical pallet bay)
|
|
42
|
+
* - multi: small stack (up to 4, e.g. a multi-deep tray)
|
|
43
|
+
* - bulk: unlimited (e.g. a floor area measured in slots)
|
|
44
|
+
*/
|
|
45
|
+
export type RackCellType = 'single' | 'multi' | 'bulk'
|
|
46
|
+
|
|
47
|
+
const NATURE: ComponentNature = {
|
|
48
|
+
mutable: false,
|
|
49
|
+
resizable: false,
|
|
50
|
+
rotatable: false,
|
|
51
|
+
properties: [
|
|
52
|
+
{
|
|
53
|
+
type: 'string',
|
|
54
|
+
label: 'cell-id',
|
|
55
|
+
name: 'cellId',
|
|
56
|
+
placeholder: 'e.g. 0-0-0'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'select',
|
|
60
|
+
label: 'cell-type',
|
|
61
|
+
name: 'cellType',
|
|
62
|
+
property: {
|
|
63
|
+
options: [
|
|
64
|
+
{ display: 'Single', value: 'single' },
|
|
65
|
+
{ display: 'Multi', value: 'multi' },
|
|
66
|
+
{ display: 'Bulk', value: 'bulk' }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
help: 'scene/component/rack-cell'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* RackCell — single-slot storage cell inside an AsrsRack.
|
|
76
|
+
*
|
|
77
|
+
* Mixin chain: CarrierHolder(ContainerAbstract)
|
|
78
|
+
* - CarrierHolder: publishes attachPointFor(), gates containable() to Carriables
|
|
79
|
+
* - ContainerAbstract: manages child carrier components
|
|
80
|
+
*
|
|
81
|
+
* No Placeable mixin — RackCell3D self-positions from the parent rack's
|
|
82
|
+
* CellMap (via updateTransform override), bypassing things-scene's standard
|
|
83
|
+
* 2D→3D coordinate mapping which cannot express 3D levels.
|
|
84
|
+
*/
|
|
85
|
+
@sceneComponent('rack-cell')
|
|
86
|
+
export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
87
|
+
// ── Identification ────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
get cellId(): string {
|
|
90
|
+
return (this.state.cellId as string) || ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get cellType(): RackCellType {
|
|
94
|
+
return ((this.state.cellType as RackCellType) || 'single')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Maximum carrier count for this cell based on cellType. */
|
|
98
|
+
get capacity(): number {
|
|
99
|
+
switch (this.cellType) {
|
|
100
|
+
case 'single': return 1
|
|
101
|
+
case 'multi': return 4
|
|
102
|
+
case 'bulk': return Infinity
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Interface ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
get nature(): ComponentNature {
|
|
109
|
+
return NATURE
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get anchors(): [] {
|
|
113
|
+
return []
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Transfer protocol ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** True when fewer carriers are currently held than capacity. */
|
|
119
|
+
canReceive(_component?: any): boolean {
|
|
120
|
+
const occupied = ((this as any).components as Component[] | undefined)?.length ?? 0
|
|
121
|
+
return occupied < this.capacity
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Accept a carrier into this cell.
|
|
126
|
+
* Sets TRANSFER_SLOT_KEY = cellId on the carrier, then reparents.
|
|
127
|
+
* Fires 'transfer-received' so monitors can react.
|
|
128
|
+
*/
|
|
129
|
+
async receive(carrier: any, options: any = {}): Promise<void> {
|
|
130
|
+
if (!this.canReceive(carrier)) {
|
|
131
|
+
;(this as any).trigger?.('transfer-rejected', {
|
|
132
|
+
type: 'transfer-rejected',
|
|
133
|
+
component: carrier,
|
|
134
|
+
container: this,
|
|
135
|
+
reason: 'no-slot'
|
|
136
|
+
})
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
carrier[TRANSFER_SLOT_KEY] = this.cellId
|
|
140
|
+
;(this as any).reparent?.(carrier, options)
|
|
141
|
+
;(this as any).trigger?.('transfer-received', {
|
|
142
|
+
type: 'transfer-received',
|
|
143
|
+
component: carrier,
|
|
144
|
+
container: this,
|
|
145
|
+
slotId: this.cellId
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Release a carrier from this cell to `target`.
|
|
151
|
+
* Delegates to `target.receive()` if available, otherwise `target.reparent()`.
|
|
152
|
+
*/
|
|
153
|
+
async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
|
|
154
|
+
if (target?.canReceive && !target.canReceive(carrier)) {
|
|
155
|
+
;(this as any).trigger?.('transfer-rejected', {
|
|
156
|
+
type: 'transfer-rejected',
|
|
157
|
+
component: carrier,
|
|
158
|
+
container: this,
|
|
159
|
+
reason: 'target-full'
|
|
160
|
+
})
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
delete carrier[TRANSFER_SLOT_KEY]
|
|
164
|
+
if (typeof target?.receive === 'function') {
|
|
165
|
+
await target.receive(carrier, options)
|
|
166
|
+
} else {
|
|
167
|
+
;(target as any).reparent?.(carrier, options)
|
|
168
|
+
}
|
|
169
|
+
;(this as any).trigger?.('transfer-dispatched', {
|
|
170
|
+
type: 'transfer-dispatched',
|
|
171
|
+
component: carrier,
|
|
172
|
+
container: this,
|
|
173
|
+
target
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Domain aliases ────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** Alias for receive() — semantic sugar for the storage domain. */
|
|
180
|
+
store(carrier: any, options?: any): Promise<void> {
|
|
181
|
+
return this.receive(carrier, options)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Alias for dispatch() — semantic sugar for the storage domain. */
|
|
185
|
+
retrieve(carrier: any, target: any, options?: any): Promise<void> {
|
|
186
|
+
return this.dispatch(carrier, target, options)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── 3D attach frame ───────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return the 3D attach frame for carriers placed in this cell.
|
|
193
|
+
* Carriers are lifted by their own halfDepth so the bottom face
|
|
194
|
+
* rests at the cell's Y-center (which is levelHeight/2 above the beam).
|
|
195
|
+
*/
|
|
196
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
197
|
+
const root = (this as any)._realObject?.object3d
|
|
198
|
+
if (!root) return null
|
|
199
|
+
const carrierDepth = resolveCarrierDepth(carrier)
|
|
200
|
+
return {
|
|
201
|
+
attach: root,
|
|
202
|
+
localPosition: { x: 0, y: carrierDepth / 2, z: 0 }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── 2D rendering ──────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/** RackCell has no 2D visual — the rack draws its own structure. */
|
|
209
|
+
render(_ctx: CanvasRenderingContext2D) {
|
|
210
|
+
// intentional no-op
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
buildRealObject(): RealObject | undefined {
|
|
216
|
+
return new RackCell3D(this as any)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveCarrierDepth(c: Component): number {
|
|
221
|
+
const eff = (c as any)._realObject?.effectiveDepth
|
|
222
|
+
if (typeof eff === 'number' && Number.isFinite(eff)) return eff
|
|
223
|
+
return numOr((c as any)?.state?.depth, 0)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function numOr(v: unknown, dflt: number): number {
|
|
227
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : dflt
|
|
228
|
+
}
|
package/src/spot.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneCompone
|
|
|
33
33
|
import {
|
|
34
34
|
CarrierHolder,
|
|
35
35
|
Placeable,
|
|
36
|
+
type AttachFrame,
|
|
36
37
|
type Alignment,
|
|
37
38
|
type Heights,
|
|
38
39
|
type PlacementArchetype
|
|
@@ -58,10 +59,8 @@ const NATURE: ComponentNature = {
|
|
|
58
59
|
// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
|
|
59
60
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
60
61
|
// addObject DOM-skip gate. Spot is purely 3D.
|
|
61
|
-
const Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component
|
|
62
|
-
|
|
63
62
|
@sceneComponent('spot')
|
|
64
|
-
export default class Spot extends
|
|
63
|
+
export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
65
64
|
static placement: PlacementArchetype = 'floor'
|
|
66
65
|
static align: Alignment = 'bottom'
|
|
67
66
|
static defaultDepth = (_h: Heights) => 2 // a thin pad
|
|
@@ -147,10 +146,10 @@ export default class Spot extends Base {
|
|
|
147
146
|
* falling back to raw `state.depth` for components built before
|
|
148
147
|
* RealObject creation.
|
|
149
148
|
*/
|
|
150
|
-
attachPointFor(carrier: Component) {
|
|
149
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
151
150
|
const ro = (this as any)._realObject as Spot3D | undefined
|
|
152
151
|
const frame = ro?.getAttachFrame?.()
|
|
153
|
-
if (!frame) return
|
|
152
|
+
if (!frame) return null
|
|
154
153
|
const carrierDepth = resolveDepth(carrier)
|
|
155
154
|
return {
|
|
156
155
|
attach: frame,
|
package/src/templates/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* things-scene catalog templates for the storage domain — pallet/box/parcel
|
|
3
|
-
* variants, ASRS rack/crane, and the virtual `spot` placement marker.
|
|
3
|
+
* variants, ASRS rack/crane, ASRS aisle composite, and the virtual `spot` placement marker.
|
|
4
4
|
*/
|
|
5
5
|
import spot from './spot.js'
|
|
6
6
|
const pallet = new URL('../../icons/pallet.png', import.meta.url).href
|
|
@@ -109,5 +109,47 @@ export default [
|
|
|
109
109
|
carriageHeight: 100
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
type: 'group',
|
|
114
|
+
description: 'AS/RS aisle — 4-level rack pair + stacker crane',
|
|
115
|
+
group: 'storage',
|
|
116
|
+
icon: asrsRack,
|
|
117
|
+
model: {
|
|
118
|
+
type: 'group',
|
|
119
|
+
top: 100,
|
|
120
|
+
left: 100,
|
|
121
|
+
width: 840,
|
|
122
|
+
height: 220,
|
|
123
|
+
components: [
|
|
124
|
+
{
|
|
125
|
+
type: 'asrs-rack',
|
|
126
|
+
top: 100,
|
|
127
|
+
left: 100,
|
|
128
|
+
width: 800,
|
|
129
|
+
height: 80,
|
|
130
|
+
levels: 4,
|
|
131
|
+
bays: 8
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'asrs-crane',
|
|
135
|
+
top: 140,
|
|
136
|
+
left: 420,
|
|
137
|
+
width: 60,
|
|
138
|
+
height: 220,
|
|
139
|
+
status: 'idle',
|
|
140
|
+
carriageHeight: 0
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'asrs-rack',
|
|
144
|
+
top: 320,
|
|
145
|
+
left: 100,
|
|
146
|
+
width: 800,
|
|
147
|
+
height: 80,
|
|
148
|
+
levels: 4,
|
|
149
|
+
bays: 8
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
112
154
|
spot
|
|
113
155
|
]
|
package/test/setup.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Browser polyfills for Node.js test environment.
|
|
3
|
+
* Required when importing @hatiolab/things-scene bundle which uses browser globals at module scope.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
var noop = function () {}
|
|
7
|
+
|
|
8
|
+
// Image polyfill (used at module scope in move-handle.ts)
|
|
9
|
+
if (typeof global.Image === 'undefined') {
|
|
10
|
+
global.Image = class Image {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.onload = null
|
|
13
|
+
this.onerror = null
|
|
14
|
+
this.src = ''
|
|
15
|
+
this.width = 0
|
|
16
|
+
this.height = 0
|
|
17
|
+
}
|
|
18
|
+
set src(v) { this._src = v }
|
|
19
|
+
get src() { return this._src || '' }
|
|
20
|
+
addEventListener(evt, fn) {}
|
|
21
|
+
removeEventListener() {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// HTMLCanvasElement polyfill
|
|
26
|
+
if (typeof global.HTMLCanvasElement === 'undefined') {
|
|
27
|
+
global.HTMLCanvasElement = class HTMLCanvasElement {
|
|
28
|
+
constructor() { this.width = 0; this.height = 0 }
|
|
29
|
+
getContext() { return null }
|
|
30
|
+
toDataURL() { return '' }
|
|
31
|
+
addEventListener() {}
|
|
32
|
+
removeEventListener() {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// HTMLElement / HTMLVideoElement stubs
|
|
37
|
+
if (typeof global.HTMLElement === 'undefined') {
|
|
38
|
+
global.HTMLElement = class HTMLElement {}
|
|
39
|
+
}
|
|
40
|
+
if (typeof global.HTMLVideoElement === 'undefined') {
|
|
41
|
+
global.HTMLVideoElement = class HTMLVideoElement {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MutationObserver polyfill
|
|
45
|
+
if (typeof global.MutationObserver === 'undefined') {
|
|
46
|
+
global.MutationObserver = class MutationObserver {
|
|
47
|
+
constructor(callback) {}
|
|
48
|
+
observe() {}
|
|
49
|
+
disconnect() {}
|
|
50
|
+
takeRecords() { return [] }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ResizeObserver polyfill
|
|
55
|
+
if (typeof global.ResizeObserver === 'undefined') {
|
|
56
|
+
global.ResizeObserver = class ResizeObserver {
|
|
57
|
+
constructor(callback) {}
|
|
58
|
+
observe() {}
|
|
59
|
+
unobserve() {}
|
|
60
|
+
disconnect() {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Mini-DOM Node implementation
|
|
65
|
+
function createNode(nodeType, tagName) {
|
|
66
|
+
var node = {
|
|
67
|
+
nodeType: nodeType,
|
|
68
|
+
tagName: tagName || '',
|
|
69
|
+
parentNode: null,
|
|
70
|
+
parentElement: null,
|
|
71
|
+
childNodes: [],
|
|
72
|
+
firstChild: null,
|
|
73
|
+
lastChild: null,
|
|
74
|
+
nextSibling: null,
|
|
75
|
+
previousSibling: null,
|
|
76
|
+
style: nodeType === 1 ? new Proxy({}, {
|
|
77
|
+
set: function(t,p,v){ t[p]=v; return true },
|
|
78
|
+
get: function(t,p){ if(p==='setProperty') return function(k,v){ t[k]=v }; return t[p] || '' }
|
|
79
|
+
}) : {},
|
|
80
|
+
textContent: '',
|
|
81
|
+
data: '',
|
|
82
|
+
setAttribute: noop,
|
|
83
|
+
getAttribute: function () { return null },
|
|
84
|
+
removeAttribute: noop,
|
|
85
|
+
addEventListener: noop,
|
|
86
|
+
removeEventListener: noop,
|
|
87
|
+
getContext: function() { return null },
|
|
88
|
+
contains: function () { return false },
|
|
89
|
+
querySelector: function() { return null },
|
|
90
|
+
querySelectorAll: function() { return [] },
|
|
91
|
+
|
|
92
|
+
appendChild: function (child) {
|
|
93
|
+
if (child.nodeType === 11) {
|
|
94
|
+
while (child.childNodes.length > 0) node.appendChild(child.childNodes[0])
|
|
95
|
+
return child
|
|
96
|
+
}
|
|
97
|
+
if (child.parentNode) child.parentNode.removeChild(child)
|
|
98
|
+
node.childNodes.push(child)
|
|
99
|
+
child.parentNode = node
|
|
100
|
+
child.parentElement = nodeType === 1 ? node : null
|
|
101
|
+
if (node.childNodes.length > 1) {
|
|
102
|
+
var prev = node.childNodes[node.childNodes.length - 2]
|
|
103
|
+
prev.nextSibling = child
|
|
104
|
+
child.previousSibling = prev
|
|
105
|
+
}
|
|
106
|
+
child.nextSibling = null
|
|
107
|
+
node.firstChild = node.childNodes[0]
|
|
108
|
+
node.lastChild = child
|
|
109
|
+
return child
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
removeChild: function (child) {
|
|
113
|
+
var idx = node.childNodes.indexOf(child)
|
|
114
|
+
if (idx === -1) return child
|
|
115
|
+
node.childNodes.splice(idx, 1)
|
|
116
|
+
if (child.previousSibling) child.previousSibling.nextSibling = child.nextSibling
|
|
117
|
+
if (child.nextSibling) child.nextSibling.previousSibling = child.previousSibling
|
|
118
|
+
child.parentNode = null; child.parentElement = null
|
|
119
|
+
child.previousSibling = null; child.nextSibling = null
|
|
120
|
+
node.firstChild = node.childNodes[0] || null
|
|
121
|
+
node.lastChild = node.childNodes[node.childNodes.length - 1] || null
|
|
122
|
+
return child
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
remove: function () { if (node.parentNode) node.parentNode.removeChild(node) },
|
|
126
|
+
|
|
127
|
+
insertBefore: function (newChild, refChild) {
|
|
128
|
+
if (!refChild) return node.appendChild(newChild)
|
|
129
|
+
if (newChild.nodeType === 11) {
|
|
130
|
+
var children = newChild.childNodes.slice()
|
|
131
|
+
for (var i = 0; i < children.length; i++) node.insertBefore(children[i], refChild)
|
|
132
|
+
return newChild
|
|
133
|
+
}
|
|
134
|
+
if (newChild.parentNode) newChild.parentNode.removeChild(newChild)
|
|
135
|
+
var idx = node.childNodes.indexOf(refChild)
|
|
136
|
+
if (idx === -1) return node.appendChild(newChild)
|
|
137
|
+
node.childNodes.splice(idx, 0, newChild)
|
|
138
|
+
newChild.parentNode = node
|
|
139
|
+
newChild.parentElement = nodeType === 1 ? node : null
|
|
140
|
+
newChild.nextSibling = refChild
|
|
141
|
+
newChild.previousSibling = refChild.previousSibling
|
|
142
|
+
if (refChild.previousSibling) refChild.previousSibling.nextSibling = newChild
|
|
143
|
+
refChild.previousSibling = newChild
|
|
144
|
+
node.firstChild = node.childNodes[0]
|
|
145
|
+
node.lastChild = node.childNodes[node.childNodes.length - 1]
|
|
146
|
+
return newChild
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
cloneNode: function (deep) {
|
|
150
|
+
var clone = createNode(nodeType, tagName)
|
|
151
|
+
clone.data = node.data; clone.textContent = node.textContent
|
|
152
|
+
if (deep && node.childNodes.length) {
|
|
153
|
+
for (var i = 0; i < node.childNodes.length; i++) clone.appendChild(node.childNodes[i].cloneNode(true))
|
|
154
|
+
}
|
|
155
|
+
return clone
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
replaceChild: function (newChild, oldChild) {
|
|
159
|
+
node.insertBefore(newChild, oldChild); node.removeChild(oldChild); return oldChild
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (nodeType === 1) {
|
|
164
|
+
node.children = []
|
|
165
|
+
if (tagName === 'template') {
|
|
166
|
+
node.content = createNode(11, '')
|
|
167
|
+
Object.defineProperty(node, 'innerHTML', {
|
|
168
|
+
get: function () { return '' },
|
|
169
|
+
set: function (html) { node.content = createNode(11, '') }
|
|
170
|
+
})
|
|
171
|
+
} else {
|
|
172
|
+
node.innerHTML = ''
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return node
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// document polyfill
|
|
179
|
+
if (typeof global.document === 'undefined') {
|
|
180
|
+
global.document = {
|
|
181
|
+
documentElement: {},
|
|
182
|
+
fonts: { ready: Promise.resolve() },
|
|
183
|
+
createElement: function (tagName) { return createNode(1, tagName) },
|
|
184
|
+
createComment: function (data) { var n = createNode(8, ''); n.data = data || ''; return n },
|
|
185
|
+
createTextNode: function (data) { var n = createNode(3, ''); n.data = data || ''; return n },
|
|
186
|
+
createDocumentFragment: function () { return createNode(11, '') },
|
|
187
|
+
createTreeWalker: function (root, whatToShow) {
|
|
188
|
+
var walker = {
|
|
189
|
+
currentNode: root,
|
|
190
|
+
nextNode: function () {
|
|
191
|
+
var cur = walker.currentNode
|
|
192
|
+
if (!cur) return null
|
|
193
|
+
if (cur.childNodes && cur.childNodes.length > 0) { walker.currentNode = cur.childNodes[0]; return walker.currentNode }
|
|
194
|
+
var node = cur
|
|
195
|
+
while (node && node !== root) {
|
|
196
|
+
if (node.nextSibling) { walker.currentNode = node.nextSibling; return walker.currentNode }
|
|
197
|
+
node = node.parentNode
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return walker
|
|
203
|
+
},
|
|
204
|
+
importNode: function (node, deep) { return node && node.cloneNode ? node.cloneNode(deep) : node },
|
|
205
|
+
adoptNode: function (node) { return node },
|
|
206
|
+
querySelector: function() { return null },
|
|
207
|
+
querySelectorAll: function() { return [] },
|
|
208
|
+
body: createNode(1, 'body'),
|
|
209
|
+
head: createNode(1, 'head')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// getComputedStyle polyfill
|
|
214
|
+
if (typeof global.getComputedStyle !== 'function') {
|
|
215
|
+
global.getComputedStyle = function () {
|
|
216
|
+
return { getPropertyValue: function () { return '' }, fontFamily: 'sans-serif' }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// window polyfill
|
|
221
|
+
if (typeof global.window === 'undefined') {
|
|
222
|
+
global.window = global
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
global.addEventListener = global.addEventListener || noop
|
|
226
|
+
global.removeEventListener = global.removeEventListener || noop
|
|
227
|
+
|
|
228
|
+
if (!global.window.location) {
|
|
229
|
+
global.window.location = { hostname: 'localhost', href: 'http://localhost/' }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// screen polyfill
|
|
233
|
+
if (typeof global.screen === 'undefined') {
|
|
234
|
+
global.screen = { width: 1280, height: 800 }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// requestAnimationFrame polyfill
|
|
238
|
+
if (typeof global.requestAnimationFrame === 'undefined') {
|
|
239
|
+
var lastTime = 0
|
|
240
|
+
global.requestAnimationFrame = function (callback) {
|
|
241
|
+
var currTime = Date.now()
|
|
242
|
+
var timeToCall = Math.max(0, 16 - (currTime - lastTime))
|
|
243
|
+
var id = setTimeout(function () { callback(currTime + timeToCall) }, timeToCall)
|
|
244
|
+
lastTime = currTime + timeToCall
|
|
245
|
+
return id
|
|
246
|
+
}
|
|
247
|
+
global.cancelAnimationFrame = function (id) { clearTimeout(id) }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// performance polyfill
|
|
251
|
+
if (typeof global.performance === 'undefined') {
|
|
252
|
+
global.performance = {}
|
|
253
|
+
}
|
|
254
|
+
if (!global.performance.now) {
|
|
255
|
+
var nowOffset = Date.now()
|
|
256
|
+
global.performance.now = function () { return Date.now() - nowOffset }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// WebGL stub (Three.js renderer check)
|
|
260
|
+
if (typeof global.WebGLRenderingContext === 'undefined') {
|
|
261
|
+
global.WebGLRenderingContext = class WebGLRenderingContext {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// CSS polyfill
|
|
265
|
+
if (typeof global.CSS === 'undefined') {
|
|
266
|
+
global.CSS = { supports: function() { return false } }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// CustomEvent polyfill
|
|
270
|
+
if (typeof global.CustomEvent === 'undefined') {
|
|
271
|
+
global.CustomEvent = class CustomEvent {
|
|
272
|
+
constructor(type, init) { this.type = type; this.detail = (init || {}).detail }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// URL polyfill (Node.js has this but in some versions it may be missing from global)
|
|
277
|
+
if (typeof global.URL === 'undefined') {
|
|
278
|
+
global.URL = require('url').URL
|
|
279
|
+
}
|