A world is full of things made many times from one design (twelve lanterns along the walkway, six rocks scattered hundreds of times), things made once (the cottage), and things made once and wanted again later (the amphora, later ten bowling pins). ConeScript has no separate notion of "type" for world-made things. Any part can be a design, and naming it makes a copy of it. A copy takes everything the design holds except its frame, follows the design when the design changes, and states only where it differs. The same mechanism, run while the world runs, is spawning.
Why: what world designers need
Under the question "is this a type or an instance?" lie eleven needs:
- Make many from one design (the lanterns).
- Change the design once, and every copy follows.
- Let one copy differ without cutting it loose: everything it does not change still follows the design.
- Make a new design from an old one (the wall lantern: the lantern's head at 0.7, on a bracket).
- Make one-offs without ceremony (the cottage exists once).
- Turn a one-off into a design later (one amphora, then ten pins; one lovely bench, then six).
- Make copies while the world runs (a splat, a dropped lantern, an army).
- Each copy is itself: its own name, path and changing state.
- Find all the copies of a design ("every lantern lights at dusk").
- Variety without hand work (rocks differing by size and seed).
- Copies are cheap (one mesh drawn forty times).
Nobody decides up front whether a thing is a type: build something concrete, and make it reusable whenever you like. ConeScript mixes pieces of several paradigms to meet the list rather than adopting one: a little inheritance (a new design from an old), a little name folding as Cone's modules do (a copy's own names are found before the design's), a little prototype (any part can be copied).
How you write it
| Form | Meaning |
|---|---|
part p: d, at: (x, y, z) | A copy of the part d, placed. It takes everything d holds except its frame, and follows d when d changes. A design named bare is copied unchanged. |
part p: d(field: value, ...), at: ... | A copy that states its differences: d called with values for its fields. A field without a default may be given by position, in declared order; a field with a default is named (the argument rule). |
part p: d(...) with lines indented under it | A copy that adds parts, fields or rules: a new design from an old one. |
part d with no at: | A design not itself in the world. It is still a part with a path and fields. |
field f: ..., common: true | One value for the design and all its copies (fields.html). |
spawn(d(...), under: parent, at: ...) | The same copy, made while the world runs. Returns the new part (actions.html). |
A package's tool (a point light, noiserock.rockMesh) is called the same way; a design reached by a path is named by it: people.tallSlimMan, or people.averageMan(build: (height: 1.9 m)).
The lanterns, the cottage, the pins and the rocks
The head both kinds of lantern share is a design of its own, built on whatever mount it is given, a stake or a bracket. The bench and the boss are illustrations. Loops are written in Cone's form; 0 ..< 6 is the six numbers 0 to 5. The shapes the lanterns draw (panes, filament, roof, tracery, solarPlates, stake, wallBracket) are functions written inside the head and its copies, written out on The lanterns; a part draws a call of one, passing what varies (size), and the shape reads the head's fixed common measurements by name.
part lanternHead // a design: no frame, so not itself in the world
field size : number = 1, within: 0.5 ..= 1 // own (the default kind): each copy its own
field panel : ?shape = None // own: the mount's solar panel, or none
field panelIn : material = materials.lanternHead // own: what the panel is drawn in
field hangsAt : place = (0, 0, 0) // own: where the head sits on its mount
field nearGain : number = 1.45, within: 1 ..= 2, common: true // fixed and common: how much brighter near it
field gain : number = 1, within: 1 ..= nearGain, mutable: true // own: how much brighter this one is drawn now
field cdPerNit : candelaPerNit = 0.45 candelaPerNit, within: 0 ..= 2, common: true // common: one value for the design and its copies
field top : meters = 0.462 m, common: true // fixed and common: the head's top, the solar panel's face
fn solarPlates(gap: meters) shape { // a panel on the top block, 4 mm thick, 8.4 cm square
let y = top - 4 mm ..= top;
if gap > 0 m { // two plates, gap apart: the block shows between
join([
box(x: -0.042 ..= 0.042, y: y, z: -0.042 ..= -gap / 2),
box(x: -0.042 ..= 0.042, y: y, z: gap / 2 ..= 0.042)
])
} else {
box(x: -0.042 ..= 0.042, y: y, z: -0.042 ..= 0.042) // one plate
}
}
part glass: panes(size), material: materials.lanternGlass, at: hangsAt
part bulb : filament(size), material: materials.lanternBulb, at: hangsAt, castsShadow: false
part cap : roof(size), material: materials.lanternHead, at: hangsAt
part plate: tracery(size), material: materials.lanternHead, at: hangsAt, castsShadow: false
if let Some(p) = panel { // a panel on the roof's top block, in its own material
part solar: scale(p, by: size), material: panelIn, at: hangsAt
}
part light: pointLight(color: color(1, 0.14, 0.01), source: 2 cm, reach: 5 m, castsShadow: true),
at: hangsAt + (0, 0.335 m * size, 0)
part inner: pointLight(color: color(1, 0.397, 0.0953), source: 2 cm, reach: 0.35 m, layers: [interior]),
at: hangsAt + (0, 0.335 m * size, 0)
derive(glass.tint, gain)
derive(bulb.tint, gain)
derive(light.intensity, materials.lanternBulb.glow * cdPerNit * gain)
derive(inner.intensity, materials.lanternBulb.glow * 0.006 candelaPerNit * gain)
part pathLantern: lanternHead(panel: Some(solarPlates(gap: 2 mm)), // the head on a stake: two plates,
panelIn: materials.lanternBlack) // black
field near: bool = false, lives: client // added, own: each visitor's own (derived: not mutable)
part post: stake(), material: materials.lanternBlack // added: the stake
area(self, sphere(radius: 4 m, at: (0, 0.2, 0)))
derive(near, occupied(post, by: me))
on(created, start(chase(gain, toward: if near && lampLevel > 0 { nearGain } else { 1 },
speed: rate(nearGain - 1, per: sim(0.4 s))))) // a rule on the design: runs per copy
part pathLanterns // a frameless holder for the twelve copies
each p in 0 ..< 6 {
each (k, side) in [(0, -1), (1, 1)] { // left, then right, of the walkway
let way = land.walkway;
let spot = way.spotAt(way.fromEnd(1.8 m + 2.8 m * p), across: side * 1.5 m); // a point on the ground
part lantern[2 * p + k]: pathLantern, at: spot // states nothing else
}
}
part wallLantern: lanternHead(size: 0.7, panel: Some(solarPlates(gap: 0 m)), // one plate, bronze
hangsAt: (0, -(top * 0.7) - 4 mm, 0.17), cdPerNit: 1.2 candelaPerNit) // a new design from the old
part bracket: wallBracket(), material: materials.lanternMetal // added: a wall plate, an arm 17 cm out
part cottage, at: (-16, land.height((-16, 0, 330)), 330) // a one-off: everything stated here, once
...
part porch
part wallLantern1: wallLantern, at: (-0.7, 2.4, 0)
part wallLantern2: wallLantern, at: (0.85, 2.4, 0)
part amphora, at: (-13.4, land.height((-13.4, 0, 337)), 337) // built as a one-off ...
field tracedOutline: List[(meters, meters)] = [...] // 215 (radius, height) points traced from a photograph
field handleCenter: List[point] = [...] // ten points traced along a handle's middle, in the plane through the axis at +x
fn handle() shape { // a round tube swept along the traced centerline, its ends carried 3 cm into the jar
uvAt(sweep(profile: outline.circle(radius: 2.66 cm),
spine: spine.through(points: handleCenter, kind: uniform)),
p => (0.5, 0.02)) // its texture coordinates on the glaze's black band at the foot
}
part jar: join([lathe(profile: tracedOutline, steps: 128), handle(), turn(handle(), by: 180 deg)]),
material: materials.amphoraPaint // jar and both handles are one mesh, in one material
part bowlingPins // ... and copied later: a frameless holder for the ten pins
each k in 0 ..< 10 {
part pin[k]: ninepin, at: pinSpot(k), scale: 0.6 // ninepin: the amphora as a prop (physics.html); pinSpot: a place on the ground
}
part bench, at: (...) // illustration
part bench2: bench, at: (...) // copied later, unchanged
part rocks // a design: one call to the noiserock package
field width : meters
field height : meters
field depth : meters
field facets : count
field facetDepth: fraction
field bumpAmp : fraction
field seed : count
part stone: noiserock.rockMesh(width: width, height: height, depth: depth, facets: facets, facetDepth: facetDepth,
bumpAmp: bumpAmp, seed: seed, rings: 9, slices: 14),
material: materials.granite
part squat1: rocks(width: 0.5, height: 0.3, depth: 0.42, facets: 10, facetDepth: 0.7, bumpAmp: 0.12, seed: 500)
part squat2: squat1(seed: 551)
part tall1 : rocks(width: 0.34, height: 0.55, depth: 0.32, facets: 8, facetDepth: 0.66, bumpAmp: 0.1, seed: 517)
part tall2 : tall1(seed: 568)
part long1 : rocks(width: 0.62, height: 0.32, depth: 0.3, facets: 9, facetDepth: 0.68, bumpAmp: 0.16, seed: 534)
part long2 : long1(seed: 585)
part boss // illustration
field health: fraction = 1, within: 0 ..= 1, mutable: true
part army // empty at load; filled while the world runs
on(became(health < 0.5), {
each i in 0 ..< 20 {
spawn(troll(stance: oneOf([guard, charge, flank], seed: i)),
under: army, at: ring(self, 6 m, i / 20)); // army.troll[0] ... troll[19]
}
})
What it means
A copy takes everything but the frame, and follows
pathLantern holds the same glass, bulb, cap, plate, two lights, fields and formulas as lanternHead, without anyone writing them again. Anything it does not state it takes from the design; if the head's design changes later (a different pane), every copy changes with it (needs 1, 2, 3). Nothing about lanternHead marks it as a type: it becomes a design only because something copies it.
A design is a part with no frame, or any placed part
A part with no at: has no place, so it is not itself in the world; it is still a part with a path and fields. But a placed part is a design too, from the moment something copies it: bench is both a bench in the world and, from the line part bench2: bench, ... on, a design. Copying a placed part copies everything but its frame, so a design and its first use can be the same thing (needs 5, 6).
A copy states only its differences
Each of the twelve path lanterns states only its frame. A copy that wanted a smaller head would write pathLantern(size: 0.85), at: ... (an illustration: the twelve are identical) and would still follow every other change to the design. Reading a copy shows exactly what is particular to it, as a designer's tool shows a prefab's overrides (need 3).
A copy's values are worked out inside the copy: the names in D(...) are looked up first in what the copy holds, which is everything D holds, and then outward as any name is (Expressions: how a name is found). So a copy may name the design's fixed fields and shapes in the values it gives: the wall lantern hangs its head -(top * 0.7) - 4 mm below the bracket's top, reading the head's height top, and passes solarPlates(gap: 0 m), a shape the head defines, as its panel.
A new design is a copy that adds and changes
The wall lantern is the same head at 0.7, with a bronze panel on its roof where the path lantern's is black, hung from a bracket instead of standing on a stake, with a stronger light. It adds the bracket. It has no area and its gain never moves, because it adds no nearness rule. There is no second mechanism for "subclassing" (need 4).
Own and common fields
An own field has a value per copy (size, panel, hanging place, gain); a common field has one value held by the design and read by every copy (cdPerNit, nearGain). The lamp level is kept on the world, not on the lanterns, because the cottage's lit windows follow it too; common is shown on values that really are the lanterns' alone (fields.html).
A copy that sets a common field starts a family of its own
D(field: v), where field is one of D's common fields, gives the new design its own common value v, shared by the new design and its own copies. D and D's other copies keep theirs. So wallLantern: lanternHead(..., cdPerNit: 1.2 candelaPerNit) makes 1.2 the one value of the wall lantern and both its copies, while the path lanterns still share 0.45: a change to the wall lanterns' value reaches the two of them and never the twelve. Every other common field the new design does not set (nearGain) is still the one value held by D.
Rules on a design run per copy
A rule written on a design runs for each copy: every path lantern has its own chase of its own gain, toward its own nearness. A rule that reads and writes only common fields runs once for the whole design. The author writes the rule once; the language works out how many copies of it run.
Seeds give variety
The rock design is one call to the noiserock package's rock tool from a few numbers and a seed; the record has fourteen fields and the design sets the seven that differ (the rest, and the other kinds, are on Sculpting: other modellers). Each second rock is a copy of the first that changes only its seed: squat1(seed: 551). Variety comes from a seed on a copy, not from hand work (need 10).
Copies share what is the same
A shape reads only what it is passed and values that never change, so copies that pass it equal values share one build, drawn many times: the twelve path lanterns all pass size 1, so each of their head's shapes is built once; the two wall lanterns share one build of each at 0.7. That is execution, but the language makes it visible, because the engine sees exactly what each shape is given (need 11).
Spawning is the same mechanism, later
spawn(troll(...), under: army, at: ...) makes a copy while the world runs and returns the new part. A load-time copy and a runtime spawn differ only in when. Everything a design says about its copies (own state, rules per copy, sharing) holds for spawned copies too. The army part owns them, so removing the boss removes its army (parts.html). A spawned copy's on(created, ...) runs when it is made, as a loaded copy's does (loading.html).
Finding every copy
"Every lantern" means every copy of pathLantern, wherever it is held, including copies of copies. A common field is the simplest way to reach them all; a rule on the design is another (need 9). A rule elsewhere names them with copiesOf(design): a list of every placed copy of the design in the world, copies of copies and spawned copies included; the design itself, and copies that are designs with no frame (ninepin: prop), are not in it. Where the copies are the children of one holder, holder.parts lists them too, in the order they were declared (Parts: a part's children).
each p in copiesOf(ninepin) { ... } // the ten pins, wherever they are held
each p in bowlingPins.parts { ... } // the same ten, as the holder's children
The list is read when the rule runs, so a copy spawned later is in it from then on. A pin reset rule uses it to glide each moved prop home (Physics: acting on physics).
How it connects
- Parts: any part can be a design; copies are parts with paths.
- Fields: own and common.
- Loading: building a design once for many copies; building again for a spawn.
- Actions:
spawnandremove. - Generators: copies placed by loops and scatter rules.
- The lanterns and a boss and its army: the worked examples.
Open questions
- May a design hold its own copies as children? If a copy takes everything but the frame, copying such a design copies its copies too. Either a copy skips children that are themselves copies of the design, or copies live in a separate holder, as
pathLanternsdoes above. - A copy cannot take anything away. The wall lantern has no stake, no area and no nearness; written as a copy of
pathLanternit would need to remove them. The example avoids this by making the head a design of its own; whether a copy may ever remove is open. - Names of spawned copies.
troll[0]totroll[19]come from the loop; a second fall below half health would spawn twenty more under the same names. Whetherspawnnumbers its copies itself (army.troll[20]) is open. - Which differences are "the same" for sharing. The engine must know which fields a shape reads; construction must therefore be checked against the fields it names.
- Who may change a design while the world runs. For a common field that is the point; for a construction field it means rebuilding every copy.